From e578b8c4138c91aadda096b97c6ed852722e023f Mon Sep 17 00:00:00 2001 From: Roberto Luzardo Date: Mon, 6 Jul 2026 14:27:30 -0500 Subject: [PATCH 1/2] Add web UI for false-positive-check script Wraps sonarqube_false_positives.py in a small Flask app so the project- and rule-based false-positive reports can be run and reviewed from a browser, with CSV downloads, instead of only via CLI. --- false-positive-check/README.md | 18 ++++ false-positive-check/webui/app.py | 96 +++++++++++++++++++ false-positive-check/webui/requirements.txt | 2 + .../webui/templates/base.html | 40 ++++++++ .../webui/templates/index.html | 15 +++ .../webui/templates/results.html | 53 ++++++++++ 6 files changed, 224 insertions(+) create mode 100644 false-positive-check/webui/app.py create mode 100644 false-positive-check/webui/requirements.txt create mode 100644 false-positive-check/webui/templates/base.html create mode 100644 false-positive-check/webui/templates/index.html create mode 100644 false-positive-check/webui/templates/results.html diff --git a/false-positive-check/README.md b/false-positive-check/README.md index 2ae43cc..bf25e02 100644 --- a/false-positive-check/README.md +++ b/false-positive-check/README.md @@ -166,6 +166,24 @@ Top 5 rules generating most false positives: - python:S3456: 15 issues (Severity: INFO) ``` +## Web UI + +A small Flask web UI is available under [`webui/`](webui/) if you'd rather run the +analysis from a browser than the command line. It reuses the same +`sonarqube_false_positives.py` module, so results are identical to the CLI output. + +### Running the UI + +```bash +cd false-positive-check +pip install -r webui/requirements.txt +python webui/app.py +``` + +Then open http://127.0.0.1:5050 in your browser, enter the SonarQube URL and +(optional) token, and click "Run Analysis". The page shows the project-based and +rule-based breakdowns in sortable tables and lets you download both CSV reports. + ## Compatibility Notes - SonarQube 9.9 is supported. diff --git a/false-positive-check/webui/app.py b/false-positive-check/webui/app.py new file mode 100644 index 0000000..51a4c11 --- /dev/null +++ b/false-positive-check/webui/app.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +Web UI for the SonarQube False Positive Issues Reporter. + +Wraps sonarqube_false_positives.py in a small Flask app so the analysis +can be run and reviewed from a browser instead of the command line. +""" + +import os +import sys +import tempfile +import uuid + +from flask import ( + Flask, + abort, + flash, + redirect, + render_template, + request, + send_from_directory, + url_for, +) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from sonarqube_false_positives import ( # noqa: E402 + analyze_false_positives, + count_false_positives_by_rule, + export_rule_counts_to_csv, + export_to_csv, +) + +app = Flask(__name__) +app.secret_key = os.environ.get("FLASK_SECRET_KEY", os.urandom(24)) + +REPORTS_DIR = os.path.join(tempfile.gettempdir(), "sonarqube_fp_reports") +os.makedirs(REPORTS_DIR, exist_ok=True) + + +@app.route("/", methods=["GET"]) +def index(): + return render_template("index.html") + + +@app.route("/analyze", methods=["POST"]) +def analyze(): + sonarqube_url = request.form.get("url", "").strip() + token = request.form.get("token", "").strip() + + if not sonarqube_url: + flash("SonarQube URL is required.", "error") + return redirect(url_for("index")) + + try: + project_results = analyze_false_positives(sonarqube_url, token) + rule_results = count_false_positives_by_rule(sonarqube_url, token) + except Exception as exc: + flash(f"Error while analyzing SonarQube instance: {exc}", "error") + return redirect(url_for("index")) + + run_id = uuid.uuid4().hex + project_csv = f"{run_id}_projects.csv" + rule_csv = f"{run_id}_rules.csv" + export_to_csv(project_results, os.path.join(REPORTS_DIR, project_csv)) + export_rule_counts_to_csv(rule_results, os.path.join(REPORTS_DIR, rule_csv)) + + total_issues = sum(r["issue_count"] for r in project_results) + sorted_projects = sorted(project_results, key=lambda x: x["issue_count"], reverse=True) + sorted_rules = sorted(rule_results, key=lambda x: x["count"], reverse=True) + + return render_template( + "results.html", + sonarqube_url=sonarqube_url, + projects=sorted_projects, + rules=sorted_rules, + total_projects=len(project_results), + total_issues=total_issues, + total_rules=len(rule_results), + project_csv=project_csv, + rule_csv=rule_csv, + ) + + +@app.route("/download/") +def download(filename): + # Only ever serve files this app generated into REPORTS_DIR, never an + # arbitrary path supplied by the client. + safe_name = os.path.basename(filename) + if safe_name != filename or not os.path.isfile(os.path.join(REPORTS_DIR, safe_name)): + abort(404) + return send_from_directory(REPORTS_DIR, safe_name, as_attachment=True) + + +if __name__ == "__main__": + app.run(host="127.0.0.1", port=5050, debug=False) diff --git a/false-positive-check/webui/requirements.txt b/false-positive-check/webui/requirements.txt new file mode 100644 index 0000000..30692b7 --- /dev/null +++ b/false-positive-check/webui/requirements.txt @@ -0,0 +1,2 @@ +flask +requests diff --git a/false-positive-check/webui/templates/base.html b/false-positive-check/webui/templates/base.html new file mode 100644 index 0000000..6111b81 --- /dev/null +++ b/false-positive-check/webui/templates/base.html @@ -0,0 +1,40 @@ + + + + + {% block title %}SonarQube False Positive Reporter{% endblock %} + + + +

SonarQube False Positive Reporter

+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+ + diff --git a/false-positive-check/webui/templates/index.html b/false-positive-check/webui/templates/index.html new file mode 100644 index 0000000..1487536 --- /dev/null +++ b/false-positive-check/webui/templates/index.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% block content %} +
+

Connect to a SonarQube instance and generate a report of issues marked as false positives, grouped by project and by rule.

+
+ + + + + + + +
+
+{% endblock %} diff --git a/false-positive-check/webui/templates/results.html b/false-positive-check/webui/templates/results.html new file mode 100644 index 0000000..2c3da01 --- /dev/null +++ b/false-positive-check/webui/templates/results.html @@ -0,0 +1,53 @@ +{% extends 'base.html' %} +{% block content %} +
+
+
{{ total_projects }}Projects analyzed
+
{{ total_issues }}False positive issues
+
{{ total_rules }}Rules involved
+
+

Source: {{ sonarqube_url }}

+ Download project report (CSV) + Download rule report (CSV) + Run another analysis +
+ +
+

False Positives by Project

+ + + + + + {% for p in projects %} + + + + + + + + {% endfor %} + +
ProjectFalse PositivesTotal Issues%Rules
{{ p.project_name }}{{ p.issue_count }}{{ p.total_issues }}{{ '%.2f'|format(p.percentage) }}%{{ p.rule_ids | join(', ') }}
+
+ +
+

False Positives by Rule

+ + + + + + {% for r in rules %} + + + + + + + {% endfor %} + +
RuleCountSeveritySoftware Qualities
{{ r.rule_id }}{{ r.count }}{{ r.severity }}{{ r.software_qualities | join(', ') }}
+
+{% endblock %} From 61e47198aaf1d1613638fd339e8689a2ef92dfa4 Mon Sep 17 00:00:00 2001 From: Gitar Date: Mon, 6 Jul 2026 19:38:57 +0000 Subject: [PATCH 2/2] fix: use private temp dir for reports and clean up CSVs after download Co-authored-by: Roberto Luzardo <185836695+roberto-luzardo-sonarsource@users.noreply.github.com> --- false-positive-check/webui/app.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/false-positive-check/webui/app.py b/false-positive-check/webui/app.py index 51a4c11..faa95a6 100644 --- a/false-positive-check/webui/app.py +++ b/false-positive-check/webui/app.py @@ -34,8 +34,8 @@ app = Flask(__name__) app.secret_key = os.environ.get("FLASK_SECRET_KEY", os.urandom(24)) -REPORTS_DIR = os.path.join(tempfile.gettempdir(), "sonarqube_fp_reports") -os.makedirs(REPORTS_DIR, exist_ok=True) +REPORTS_DIR = tempfile.mkdtemp(prefix="sonarqube_fp_reports_") +os.chmod(REPORTS_DIR, 0o700) @app.route("/", methods=["GET"]) @@ -87,9 +87,15 @@ def download(filename): # Only ever serve files this app generated into REPORTS_DIR, never an # arbitrary path supplied by the client. safe_name = os.path.basename(filename) - if safe_name != filename or not os.path.isfile(os.path.join(REPORTS_DIR, safe_name)): + file_path = os.path.join(REPORTS_DIR, safe_name) + if safe_name != filename or not os.path.isfile(file_path): abort(404) - return send_from_directory(REPORTS_DIR, safe_name, as_attachment=True) + response = send_from_directory(REPORTS_DIR, safe_name, as_attachment=True) + try: + os.remove(file_path) + except OSError: + pass + return response if __name__ == "__main__":