Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions false-positive-check/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
102 changes: 102 additions & 0 deletions false-positive-check/webui/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/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))
Comment thread
gitar-bot[bot] marked this conversation as resolved.

REPORTS_DIR = tempfile.mkdtemp(prefix="sonarqube_fp_reports_")
os.chmod(REPORTS_DIR, 0o700)


@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))
Comment thread
gitar-bot[bot] marked this conversation as resolved.

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/<path:filename>")
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)
file_path = os.path.join(REPORTS_DIR, safe_name)
if safe_name != filename or not os.path.isfile(file_path):
abort(404)
response = send_from_directory(REPORTS_DIR, safe_name, as_attachment=True)
try:
os.remove(file_path)
except OSError:
pass
return response


if __name__ == "__main__":
app.run(host="127.0.0.1", port=5050, debug=False)
2 changes: 2 additions & 0 deletions false-positive-check/webui/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
flask
requests
40 changes: 40 additions & 0 deletions false-positive-check/webui/templates/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}SonarQube False Positive Reporter{% endblock %}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background: #f3f5f7; color: #1a2733; }
header { background: #1a2733; color: #fff; padding: 1rem 2rem; }
header h1 { margin: 0; font-size: 1.3rem; }
main { max-width: 960px; margin: 2rem auto; padding: 0 1rem; }
.card { background: #fff; border-radius: 8px; padding: 1.5rem 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 1.5rem; }
label { display: block; font-weight: 600; margin-bottom: 0.25rem; margin-top: 1rem; }
input[type=text], input[type=password] { width: 100%; padding: 0.5rem; border: 1px solid #cdd5dc; border-radius: 4px; box-sizing: border-box; }
button { margin-top: 1.5rem; background: #4c9aff; color: #fff; border: none; padding: 0.6rem 1.4rem; border-radius: 4px; cursor: pointer; font-size: 1rem; }
button:hover { background: #2f7fe0; }
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
th, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid #e2e8ee; font-size: 0.9rem; }
th { background: #eef2f6; }
.summary { display: flex; gap: 2rem; margin-bottom: 1rem; }
.summary div { text-align: center; }
.summary strong { display: block; font-size: 1.6rem; color: #1a2733; }
.flash { padding: 0.75rem 1rem; border-radius: 4px; margin-bottom: 1rem; }
.flash.error { background: #fdecea; color: #b3261e; border: 1px solid #f5c6c3; }
a.button-link { display: inline-block; margin-top: 1rem; margin-right: 0.75rem; background: #eef2f6; color: #1a2733; padding: 0.5rem 1rem; border-radius: 4px; text-decoration: none; }
</style>
</head>
<body>
<header><h1>SonarQube False Positive Reporter</h1></header>
<main>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
</body>
</html>
15 changes: 15 additions & 0 deletions false-positive-check/webui/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% block content %}
<div class="card">
<p>Connect to a SonarQube instance and generate a report of issues marked as false positives, grouped by project and by rule.</p>
<form method="post" action="{{ url_for('analyze') }}">
<label for="url">SonarQube URL</label>
<input type="text" id="url" name="url" placeholder="http://localhost:9000" required>

<label for="token">Authentication Token (optional)</label>
<input type="password" id="token" name="token" placeholder="squ_...">

<button type="submit">Run Analysis</button>
</form>
</div>
{% endblock %}
53 changes: 53 additions & 0 deletions false-positive-check/webui/templates/results.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{% extends 'base.html' %}
{% block content %}
<div class="card">
<div class="summary">
<div><strong>{{ total_projects }}</strong>Projects analyzed</div>
<div><strong>{{ total_issues }}</strong>False positive issues</div>
<div><strong>{{ total_rules }}</strong>Rules involved</div>
</div>
<p>Source: {{ sonarqube_url }}</p>
<a class="button-link" href="{{ url_for('download', filename=project_csv) }}">Download project report (CSV)</a>
<a class="button-link" href="{{ url_for('download', filename=rule_csv) }}">Download rule report (CSV)</a>
<a class="button-link" href="{{ url_for('index') }}">Run another analysis</a>
</div>

<div class="card">
<h2>False Positives by Project</h2>
<table>
<thead>
<tr><th>Project</th><th>False Positives</th><th>Total Issues</th><th>%</th><th>Rules</th></tr>
</thead>
<tbody>
{% for p in projects %}
<tr>
<td>{{ p.project_name }}</td>
<td>{{ p.issue_count }}</td>
<td>{{ p.total_issues }}</td>
<td>{{ '%.2f'|format(p.percentage) }}%</td>
<td>{{ p.rule_ids | join(', ') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

<div class="card">
<h2>False Positives by Rule</h2>
<table>
<thead>
<tr><th>Rule</th><th>Count</th><th>Severity</th><th>Software Qualities</th></tr>
</thead>
<tbody>
{% for r in rules %}
<tr>
<td>{{ r.rule_id }}</td>
<td>{{ r.count }}</td>
<td>{{ r.severity }}</td>
<td>{{ r.software_qualities | join(', ') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}