From 3b6046b82895ea526e09022500d3b3ed5ced4be7 Mon Sep 17 00:00:00 2001 From: Roberto Luzardo Date: Wed, 11 Feb 2026 11:26:05 -0600 Subject: [PATCH 1/2] Add quality gate and quality profiles to false positive report This commit enhances the SonarQube false positive reporter to include: - Quality gate used by each project - Quality profiles containing the rules that generated false positive issues New API methods added: - get_project_quality_gate(): Fetches the quality gate for a project - get_quality_profiles_for_project(): Retrieves all quality profiles associated with a project The CSV output now includes two additional columns: - Quality Gate: The name of the quality gate assigned to the project - Quality Profiles: A semicolon-separated list of language:profile pairs Updated README with the new features and sample output. Co-Authored-By: Claude Sonnet 4.5 --- false-positive-check/README.md | 10 +- .../sonarqube_false_positives.py | 127 ++++++++++++++---- 2 files changed, 105 insertions(+), 32 deletions(-) diff --git a/false-positive-check/README.md b/false-positive-check/README.md index 4f77a6e..274403c 100644 --- a/false-positive-check/README.md +++ b/false-positive-check/README.md @@ -8,6 +8,8 @@ This tool connects to a SonarQube server, iterates through all projects, and col - Project names - Number of false positive issues per project - List of rule IDs that generated the false positive issues +- Quality gate used by each project +- Quality profiles associated with each project ## Requirements @@ -65,10 +67,10 @@ python sonarqube_false_positives.py -u -t ### Sample Output ```csv -Project Name,Number of Issues,Rule IDs -My Web Application,5,java:S1234, java:S5678 -Backend API,12,python:S9012, python:S3456, python:S7890 -Frontend Dashboard,3,javascript:S2468 +Project Name,Number of Issues,Rule IDs,Quality Gate,Quality Profiles +My Web Application,5,java:S1234, java:S5678,Sonar way,java: Sonar way +Backend API,12,python:S9012, python:S3456, python:S7890,Custom Gate,py: Custom Python Profile +Frontend Dashboard,3,javascript:S2468,Sonar way,js: Sonar way; ts: Sonar way ``` ### Console Output diff --git a/false-positive-check/sonarqube_false_positives.py b/false-positive-check/sonarqube_false_positives.py index 03434f4..4f0f34e 100644 --- a/false-positive-check/sonarqube_false_positives.py +++ b/false-positive-check/sonarqube_false_positives.py @@ -76,17 +76,17 @@ def get_all_projects(self) -> List[Dict]: def get_false_positive_issues(self, project_key: str) -> List[Dict]: """ Get all issues marked as false positives for a specific project - + Args: project_key: The project key - + Returns: List of issue dictionaries """ issues = [] page = 1 page_size = 500 - + while True: url = f"{self.base_url}/api/issues/search" params = { @@ -95,99 +95,170 @@ def get_false_positive_issues(self, project_key: str) -> List[Dict]: 'p': page, 'ps': page_size } - + try: response = self.session.get(url, params=params) response.raise_for_status() data = response.json() - + issues.extend(data.get('issues', [])) - + # Check if there are more pages paging = data.get('paging', {}) total = paging.get('total', 0) - + if page * page_size >= total: break - + page += 1 - + except requests.exceptions.RequestException as e: print(f"Error fetching issues for project {project_key}: {e}", file=sys.stderr) break - + return issues + def get_project_quality_gate(self, project_key: str) -> str: + """ + Get the quality gate associated with a project + + Args: + project_key: The project key + + Returns: + Quality gate name or 'N/A' if not found + """ + url = f"{self.base_url}/api/qualitygates/get_by_project" + params = {'project': project_key} + + try: + response = self.session.get(url, params=params) + response.raise_for_status() + data = response.json() + + quality_gate = data.get('qualityGate', {}) + return quality_gate.get('name', 'N/A') + + except requests.exceptions.RequestException as e: + print(f"Error fetching quality gate for project {project_key}: {e}", file=sys.stderr) + return 'N/A' + + def get_quality_profiles_for_project(self, project_key: str) -> Dict[str, str]: + """ + Get quality profiles associated with a project + + Args: + project_key: The project key + + Returns: + Dictionary mapping language to quality profile name + """ + url = f"{self.base_url}/api/qualityprofiles/search" + params = {'project': project_key} + + try: + response = self.session.get(url, params=params) + response.raise_for_status() + data = response.json() + + profiles = {} + for profile in data.get('profiles', []): + language = profile.get('language', 'unknown') + name = profile.get('name', 'N/A') + profiles[language] = name + + return profiles + + except requests.exceptions.RequestException as e: + print(f"Error fetching quality profiles for project {project_key}: {e}", file=sys.stderr) + return {} + def analyze_false_positives(sonarqube_url: str, token: str = None) -> List[Dict]: """ Analyze false positive issues across all projects - + Args: sonarqube_url: SonarQube server URL token: Authentication token (optional) - + Returns: List of dictionaries with project analysis results """ client = SonarQubeClient(sonarqube_url, token) - + print("Fetching all projects...") projects = client.get_all_projects() print(f"Found {len(projects)} projects") - + results = [] - + for i, project in enumerate(projects, 1): project_key = project.get('key') project_name = project.get('name', project_key) - + print(f"Processing project {i}/{len(projects)}: {project_name}") - + # Get false positive issues for this project issues = client.get_false_positive_issues(project_key) - + # Collect unique rule IDs rule_ids: Set[str] = set() for issue in issues: rule = issue.get('rule') if rule: rule_ids.add(rule) - + + # Get quality gate for the project + quality_gate = client.get_project_quality_gate(project_key) + + # Get quality profiles for the project + quality_profiles = client.get_quality_profiles_for_project(project_key) + results.append({ 'project_name': project_name, 'project_key': project_key, 'issue_count': len(issues), - 'rule_ids': sorted(list(rule_ids)) + 'rule_ids': sorted(list(rule_ids)), + 'quality_gate': quality_gate, + 'quality_profiles': quality_profiles }) - + return results def export_to_csv(results: List[Dict], output_file: str = 'sonarqube_false_positives.csv'): """ Export results to CSV file - + Args: results: List of project analysis results output_file: Output CSV file path """ with open(output_file, 'w', newline='', encoding='utf-8') as csvfile: - fieldnames = ['Project Name', 'Number of Issues', 'Rule IDs'] + fieldnames = ['Project Name', 'Number of Issues', 'Rule IDs', 'Quality Gate', 'Quality Profiles'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) - + writer.writeheader() - + for result in results: # Convert rule IDs list to comma-separated string rule_ids_str = ', '.join(result['rule_ids']) if result['rule_ids'] else '' - + + # Convert quality profiles dict to formatted string + quality_profiles_str = '' + if result.get('quality_profiles'): + profiles_list = [f"{lang}: {name}" for lang, name in result['quality_profiles'].items()] + quality_profiles_str = '; '.join(profiles_list) + writer.writerow({ 'Project Name': result['project_name'], 'Number of Issues': result['issue_count'], - 'Rule IDs': rule_ids_str + 'Rule IDs': rule_ids_str, + 'Quality Gate': result.get('quality_gate', 'N/A'), + 'Quality Profiles': quality_profiles_str }) - + print(f"\nReport exported to: {output_file}") From 82221348330e2a4c085ef16e1e3b7351a130f20f Mon Sep 17 00:00:00 2001 From: Roberto Luzardo Date: Wed, 11 Feb 2026 15:53:14 -0600 Subject: [PATCH 2/2] Add rule-based report with severity and software qualities - Add new CSV report showing false positives aggregated by rule - Include rule severity and software qualities in rule report - Add percentage column to project report showing FP ratio - Fetch all issues across projects for comprehensive rule analysis - Remove quality gate and quality profiles from project report - Update README with new features and usage examples --- false-positive-check/README.md | 77 ++++- .../sonarqube_false_positives.py | 299 ++++++++++++++++-- 2 files changed, 346 insertions(+), 30 deletions(-) diff --git a/false-positive-check/README.md b/false-positive-check/README.md index 274403c..afdeb30 100644 --- a/false-positive-check/README.md +++ b/false-positive-check/README.md @@ -1,15 +1,25 @@ # SonarQube False Positive Issues Reporter -A Python script that analyzes all projects in a SonarQube instance and generates a comprehensive CSV report of issues marked as false positives. +A Python script that analyzes all projects in a SonarQube instance and generates comprehensive CSV reports of issues marked as false positives. ## Overview -This tool connects to a SonarQube server, iterates through all projects, and collects information about issues that have been marked as false positives. It generates a CSV report containing: +This tool connects to a SonarQube server and generates two detailed CSV reports: + +### 1. Project-Based Report +Contains information about false positives per project: - Project names - Number of false positive issues per project +- Total issues in the project +- Percentage of false positives - List of rule IDs that generated the false positive issues -- Quality gate used by each project -- Quality profiles associated with each project + +### 2. Rule-Based Report +Contains aggregated statistics about rules generating false positives: +- Rule IDs +- Count of false positive issues per rule +- Rule severity +- Software qualities impacted by each rule ## Requirements @@ -62,15 +72,37 @@ python sonarqube_false_positives.py -u -t |----------|-------|----------|-------------|---------| | `--url` | `-u` | Yes | SonarQube server URL | N/A | | `--token` | `-t` | No | Authentication token | None | -| `--output` | `-o` | No | Output CSV file path | `sonarqube_false_positives.csv` | +| `--output` | `-o` | No | Output CSV file for project-based report | `sonarqube_false_positives.csv` | +| `--rule-output` | `-r` | No | Output CSV file for rule-based report | `false_positives_by_rule.csv` | + +### Examples + +```bash +# Basic usage with default output files +python sonarqube_false_positives.py -u http://localhost:9000 -t mytoken123 + +# Custom output file names +python sonarqube_false_positives.py -u https://sonarqube.example.com -t mytoken123 -o projects.csv -r rules.csv +``` + +### Sample Output Files + +#### Project-Based Report (sonarqube_false_positives.csv) -### Sample Output +```csv +Project Name,Number of Issues,Total Issues,Percentage,Rule IDs +My Web Application,5,100,5.00%,"java:S1234, java:S5678" +Backend API,12,250,4.80%,"python:S9012, python:S3456, python:S7890" +Frontend Dashboard,3,80,3.75%,javascript:S2468 +``` + +#### Rule-Based Report (false_positives_by_rule.csv) ```csv -Project Name,Number of Issues,Rule IDs,Quality Gate,Quality Profiles -My Web Application,5,java:S1234, java:S5678,Sonar way,java: Sonar way -Backend API,12,python:S9012, python:S3456, python:S7890,Custom Gate,py: Custom Python Profile -Frontend Dashboard,3,javascript:S2468,Sonar way,js: Sonar way; ts: Sonar way +Rule ID,False Positive Count,Severity,Software Qualities +java:S1234,45,MAJOR,"MAINTAINABILITY, RELIABILITY" +python:S9012,32,CRITICAL,SECURITY +javascript:S2468,21,MINOR,MAINTAINABILITY ``` ### Console Output @@ -78,10 +110,13 @@ Frontend Dashboard,3,javascript:S2468,Sonar way,js: Sonar way; ts: Sonar way During execution, the script displays: - Connection details - Progress for each project being processed +- Progress for each rule being analyzed - Summary statistics including: - Total projects analyzed - Total false positive issues + - Total unique rules generating false positives - Top 5 projects with most false positives + - Top 5 rules generating most false positives Example console output: ``` @@ -92,12 +127,25 @@ Found 25 projects Processing project 1/25: My Web Application Processing project 2/25: Backend API ... -Report exported to: custom_report.csv + +Report exported to: sonarqube_false_positives.csv + +============================================================ +Fetching all false positive issues... + Fetched 143 of 143 issues... +Total false positive issues fetched: 143 + +Fetching rule details... + Processing rule 1/15: java:S1234 + Processing rule 2/15: python:S9012 +... +Rule-wise report exported to: false_positives_by_rule.csv ============================================================ Summary: Total projects analyzed: 25 Total false positive issues: 143 +Total unique rules generating false positives: 15 Top 5 projects with most false positives: - Backend API: 45 issues @@ -105,4 +153,11 @@ Top 5 projects with most false positives: - Frontend Dashboard: 21 issues - Mobile App: 18 issues - Data Pipeline: 15 issues + +Top 5 rules generating most false positives: + - java:S1234: 45 issues (Severity: MAJOR) + - python:S9012: 32 issues (Severity: CRITICAL) + - javascript:S2468: 21 issues (Severity: MINOR) + - java:S5678: 18 issues (Severity: MAJOR) + - python:S3456: 15 issues (Severity: INFO) ``` diff --git a/false-positive-check/sonarqube_false_positives.py b/false-positive-check/sonarqube_false_positives.py index 4f0f34e..84c6df1 100644 --- a/false-positive-check/sonarqube_false_positives.py +++ b/false-positive-check/sonarqube_false_positives.py @@ -118,6 +118,83 @@ def get_false_positive_issues(self, project_key: str) -> List[Dict]: return issues + def get_total_issues_count(self, project_key: str) -> int: + """ + Get total count of all issues for a specific project + + Args: + project_key: The project key + + Returns: + Total number of issues + """ + url = f"{self.base_url}/api/issues/search" + params = { + 'componentKeys': project_key, + 'ps': 1, # We only need the total count, not the actual issues + 'p': 1 + } + + try: + response = self.session.get(url, params=params) + response.raise_for_status() + data = response.json() + + paging = data.get('paging', {}) + total = paging.get('total', 0) + return total + + except requests.exceptions.RequestException as e: + print(f"Error fetching total issues for project {project_key}: {e}", file=sys.stderr) + return 0 + + def get_all_false_positive_issues(self) -> List[Dict]: + """ + Get all issues marked as false positives across all projects + + Returns: + List of issue dictionaries + """ + issues = [] + page = 1 + page_size = 500 + + print("Fetching all false positive issues...") + + while True: + url = f"{self.base_url}/api/issues/search" + params = { + 'resolutions': 'FALSE-POSITIVE', + 'p': page, + 'ps': page_size + } + + try: + response = self.session.get(url, params=params) + response.raise_for_status() + data = response.json() + + page_issues = data.get('issues', []) + issues.extend(page_issues) + + # Check if there are more pages + paging = data.get('paging', {}) + total = paging.get('total', 0) + + print(f" Fetched {len(issues)} of {total} issues...") + + if page * page_size >= total: + break + + page += 1 + + except requests.exceptions.RequestException as e: + print(f"Error fetching all false positive issues: {e}", file=sys.stderr) + break + + print(f"Total false positive issues fetched: {len(issues)}") + return issues + def get_project_quality_gate(self, project_key: str) -> str: """ Get the quality gate associated with a project @@ -173,6 +250,104 @@ def get_quality_profiles_for_project(self, project_key: str) -> Dict[str, str]: print(f"Error fetching quality profiles for project {project_key}: {e}", file=sys.stderr) return {} + def get_rule_details(self, rule_key: str) -> Dict: + """ + Get details about a specific rule + + Args: + rule_key: The rule key (e.g., 'java:S1234') + + Returns: + Dictionary with rule details including severity and software qualities + """ + url = f"{self.base_url}/api/rules/show" + params = {'key': rule_key} + + try: + response = self.session.get(url, params=params) + response.raise_for_status() + data = response.json() + + rule = data.get('rule', {}) + + # Extract software qualities (impacts) + impacts = rule.get('impacts', []) + software_qualities = [impact.get('softwareQuality', '') for impact in impacts if impact.get('softwareQuality')] + + return { + 'severity': rule.get('severity', 'N/A'), + 'name': rule.get('name', 'N/A'), + 'language': rule.get('lang', 'N/A'), + 'software_qualities': software_qualities + } + + except requests.exceptions.RequestException as e: + print(f"Error fetching rule details for {rule_key}: {e}", file=sys.stderr) + return {'severity': 'N/A', 'name': 'N/A', 'language': 'N/A', 'software_qualities': []} + + def get_quality_profiles_with_rule(self, rule_key: str) -> List[str]: + """ + Get quality profiles where a specific rule is active + + Args: + rule_key: The rule key (e.g., 'java:S1234') + + Returns: + List of quality profile names where the rule is active + """ + url = f"{self.base_url}/api/qualityprofiles/search" + + try: + response = self.session.get(url) + response.raise_for_status() + data = response.json() + + active_profiles = [] + + for profile in data.get('profiles', []): + profile_key = profile.get('key') + profile_name = profile.get('name') + language = profile.get('language') + + # Check if rule is active in this profile + if self.is_rule_active_in_profile(rule_key, profile_key): + active_profiles.append(f"{profile_name} ({language})") + + return active_profiles + + except requests.exceptions.RequestException as e: + print(f"Error fetching quality profiles for rule {rule_key}: {e}", file=sys.stderr) + return [] + + def is_rule_active_in_profile(self, rule_key: str, profile_key: str) -> bool: + """ + Check if a rule is active in a specific quality profile + + Args: + rule_key: The rule key + profile_key: The quality profile key + + Returns: + True if the rule is active in the profile, False otherwise + """ + url = f"{self.base_url}/api/rules/search" + params = { + 'qprofile': profile_key, + 'rule_key': rule_key, + 'activation': 'true' + } + + try: + response = self.session.get(url, params=params) + response.raise_for_status() + data = response.json() + + total = data.get('total', 0) + return total > 0 + + except requests.exceptions.RequestException as e: + return False + def analyze_false_positives(sonarqube_url: str, token: str = None) -> List[Dict]: """ @@ -201,6 +376,12 @@ def analyze_false_positives(sonarqube_url: str, token: str = None) -> List[Dict] # Get false positive issues for this project issues = client.get_false_positive_issues(project_key) + + # Get total issues count for this project + total_issues = client.get_total_issues_count(project_key) + + # Calculate percentage + percentage = (len(issues) / total_issues * 100) if total_issues > 0 else 0 # Collect unique rule IDs rule_ids: Set[str] = set() @@ -209,19 +390,13 @@ def analyze_false_positives(sonarqube_url: str, token: str = None) -> List[Dict] if rule: rule_ids.add(rule) - # Get quality gate for the project - quality_gate = client.get_project_quality_gate(project_key) - - # Get quality profiles for the project - quality_profiles = client.get_quality_profiles_for_project(project_key) - results.append({ 'project_name': project_name, 'project_key': project_key, 'issue_count': len(issues), - 'rule_ids': sorted(list(rule_ids)), - 'quality_gate': quality_gate, - 'quality_profiles': quality_profiles + 'total_issues': total_issues, + 'percentage': percentage, + 'rule_ids': sorted(list(rule_ids)) }) return results @@ -236,7 +411,7 @@ def export_to_csv(results: List[Dict], output_file: str = 'sonarqube_false_posit output_file: Output CSV file path """ with open(output_file, 'w', newline='', encoding='utf-8') as csvfile: - fieldnames = ['Project Name', 'Number of Issues', 'Rule IDs', 'Quality Gate', 'Quality Profiles'] + fieldnames = ['Project Name', 'Number of Issues', 'Total Issues', 'Percentage', 'Rule IDs'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() @@ -245,23 +420,91 @@ def export_to_csv(results: List[Dict], output_file: str = 'sonarqube_false_posit # Convert rule IDs list to comma-separated string rule_ids_str = ', '.join(result['rule_ids']) if result['rule_ids'] else '' - # Convert quality profiles dict to formatted string - quality_profiles_str = '' - if result.get('quality_profiles'): - profiles_list = [f"{lang}: {name}" for lang, name in result['quality_profiles'].items()] - quality_profiles_str = '; '.join(profiles_list) - writer.writerow({ 'Project Name': result['project_name'], 'Number of Issues': result['issue_count'], - 'Rule IDs': rule_ids_str, - 'Quality Gate': result.get('quality_gate', 'N/A'), - 'Quality Profiles': quality_profiles_str + 'Total Issues': result['total_issues'], + 'Percentage': f"{result['percentage']:.2f}%", + 'Rule IDs': rule_ids_str }) print(f"\nReport exported to: {output_file}") +def count_false_positives_by_rule(sonarqube_url: str, token: str = None) -> List[Dict]: + """ + Count false positive issues by rule across all projects and gather rule details + + Args: + sonarqube_url: SonarQube server URL + token: Authentication token (optional) + + Returns: + List of dictionaries with rule ID, count, severity, and quality profiles + """ + client = SonarQubeClient(sonarqube_url, token) + + # Get all false positive issues + all_issues = client.get_all_false_positive_issues() + + # Count issues by rule + rule_counts = defaultdict(int) + for issue in all_issues: + rule = issue.get('rule') + if rule: + rule_counts[rule] += 1 + + # Gather detailed information for each rule + print("\nFetching rule details...") + rule_details = [] + + for i, (rule_id, count) in enumerate(rule_counts.items(), 1): + print(f" Processing rule {i}/{len(rule_counts)}: {rule_id}") + + # Get rule details (severity and software qualities) + details = client.get_rule_details(rule_id) + + rule_details.append({ + 'rule_id': rule_id, + 'count': count, + 'severity': details.get('severity', 'N/A'), + 'software_qualities': details.get('software_qualities', []) + }) + + return rule_details + + +def export_rule_counts_to_csv(rule_details: List[Dict], output_file: str = 'false_positives_by_rule.csv'): + """ + Export rule-wise false positive counts to CSV file + + Args: + rule_details: List of dictionaries with rule information + output_file: Output CSV file path + """ + # Sort rules by count (descending) + sorted_rules = sorted(rule_details, key=lambda x: x['count'], reverse=True) + + with open(output_file, 'w', newline='', encoding='utf-8') as csvfile: + fieldnames = ['Rule ID', 'False Positive Count', 'Severity', 'Software Qualities'] + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + + writer.writeheader() + + for rule in sorted_rules: + # Convert software qualities list to comma-separated string + qualities_str = ', '.join(rule['software_qualities']) if rule['software_qualities'] else 'N/A' + + writer.writerow({ + 'Rule ID': rule['rule_id'], + 'False Positive Count': rule['count'], + 'Severity': rule['severity'], + 'Software Qualities': qualities_str + }) + + print(f"Rule-wise report exported to: {output_file}") + + def main(): """Main entry point""" @@ -291,6 +534,11 @@ def main(): default='sonarqube_false_positives.csv', help='Output CSV file path (default: sonarqube_false_positives.csv)') + parser.add_argument( + '-r', '--rule-output', + default='false_positives_by_rule.csv', + help='Output CSV file for rule-wise counts (default: false_positives_by_rule.csv)') + args = parser.parse_args() print(f"Connecting to SonarQube at: {args.url}") @@ -303,12 +551,18 @@ def main(): # Export to CSV export_to_csv(results, args.output) + # Count false positives by rule and export to CSV + print("\n" + "=" * 60) + rule_details = count_false_positives_by_rule(args.url, args.token) + export_rule_counts_to_csv(rule_details, args.rule_output) + # Print summary print("\n" + "=" * 60) print("Summary:") print(f"Total projects analyzed: {len(results)}") total_issues = sum(r['issue_count'] for r in results) print(f"Total false positive issues: {total_issues}") + print(f"Total unique rules generating false positives: {len(rule_details)}") # Show projects with most false positives if results: @@ -318,6 +572,13 @@ def main(): if result['issue_count'] > 0: print(f" - {result['project_name']}: {result['issue_count']} issues") + # Show rules with most false positives + if rule_details: + sorted_rules = sorted(rule_details, key=lambda x: x['count'], reverse=True) + print("\nTop 5 rules generating most false positives:") + for rule in sorted_rules[:5]: + print(f" - {rule['rule_id']}: {rule['count']} issues (Severity: {rule['severity']})") + except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1)