From 39ba000bea4c1589dc7141c6c71c38aea068a478 Mon Sep 17 00:00:00 2001 From: Ripeeerr Date: Sun, 16 Aug 2026 03:16:07 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE?= =?UTF-8?q?=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D0=BE=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D1=8C=20=D0=BE=D1=82=D1=87=D0=B5=D1=82=20?= =?UTF-8?q?=D0=B2=20.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/scan.py | 79 ++++++++++++++++++---------- app/templates/reports/scan_report.md | 53 +++++++++++++++++++ 2 files changed, 105 insertions(+), 27 deletions(-) create mode 100644 app/templates/reports/scan_report.md diff --git a/app/routes/scan.py b/app/routes/scan.py index 3a6115d..c9c5d6c 100644 --- a/app/routes/scan.py +++ b/app/routes/scan.py @@ -1,6 +1,6 @@ from collections import defaultdict -from flask import Blueprint, jsonify +from flask import Blueprint, jsonify, render_template from flask_login import current_user, login_required from sqlalchemy.orm import joinedload @@ -14,10 +14,10 @@ @scan_bp.route("/api/scans/", methods=["GET"]) @login_required def scan_status(scan_id): - scan = Scan.query.filter( - Scan.id == scan_id, Project.owner_id == current_user.id - ).first() - if not scan: + scan = ( + Scan.query.options(joinedload(Scan.project)).filter(Scan.id == scan_id).first() + ) + if not scan or scan.project.owner_id != current_user.id: return jsonify({"error": "Данного скана не существует"}), 404 return jsonify( { @@ -32,20 +32,28 @@ def scan_status(scan_id): ), 200 -@scan_bp.route("/api/scans//report", methods=["GET"]) -@login_required -def report_json(scan_id): +def group_sort_severity(findings): + grouped = defaultdict(list) + for f in findings: + grouped[f["severity"]].append(f) + for finding_list in grouped.values(): + finding_list.sort(key=lambda x: x["file_path"]) + summary = { + severity: len(finding_list) for severity, finding_list in grouped.items() + } + return dict(grouped), summary + + +def findings(scan_id): scan = ( - Scan.query.options(joinedload(Scan.project)) - .filter(Scan.id == scan_id, Project.owner_id == current_user.id) - .first() + Scan.query.options(joinedload(Scan.project)).filter(Scan.id == scan_id).first() ) - if not scan: - return jsonify({"error": "Данного скана не существует"}), 404 + if not scan or scan.project.owner_id != current_user.id: + return None, None, (jsonify({"error": "Данного скана не существует"}), 404) if scan.status == "failed": - return jsonify({"error": scan.error_message}), 409 + return None, None, (jsonify({"error": scan.error_message}), 409) if scan.status != "done": - return jsonify({"status": scan.status}), 409 + return None, None, (jsonify({"status": scan.status}), 409) finding = Finding.query.filter( Scan.id == scan_id, Project.owner_id == current_user.id ).all() @@ -66,20 +74,17 @@ def report_json(scan_id): } for f in finding ] - - def group_sort_severity(findings): - grouped = defaultdict(list) - for f in findings: - grouped[f["severity"]].append(f) - for finding_list in grouped.values(): - finding_list.sort(key=lambda x: x["file_path"]) - summary = { - severity: len(finding_list) for severity, finding_list in grouped.items() - } - return dict(grouped), summary - grouped, summary = group_sort_severity(findings) + return scan, (summary, grouped), None + +@scan_bp.route("/api/scans//report", methods=["GET"]) +@login_required +def report_json(scan_id): + scan, data, err = findings(scan_id) + if err: + return err + summary, grouped = data return jsonify( { "scan_id": scan.id, @@ -91,3 +96,23 @@ def group_sort_severity(findings): "findings": grouped, } ) + + +@scan_bp.route("/api/scans//report.md", methods=["GET"]) +@login_required +def report_md(scan_id): + scan, data, err = findings(scan_id) + if err: + return err + summary, grouped = data + context = { + "summary": summary, + "grouped": grouped, + "project_title": scan.project.title, + "repo_url": scan.project.repo_url, + "scan_id": scan.id, + "commit_sha": scan.commit_sha, + "finished_at": scan.finished_at, + "truncated": scan.truncated, + } + return render_template("/reports/scan_report.md", **context) diff --git a/app/templates/reports/scan_report.md b/app/templates/reports/scan_report.md new file mode 100644 index 0000000..f4066ae --- /dev/null +++ b/app/templates/reports/scan_report.md @@ -0,0 +1,53 @@ +# Отчёт о сканировании — {{ project_title }} + +| Параметр | Значение | +|---|---| +| Проект | {{ project_title }} | +| Репозиторий | [{{ repo_url }}]({{ repo_url }}) | +| ID скана | {{ scan_id }} | +| Коммит | `{{ commit_sha }}` | +| Завершён | {{ finished_at or "—" }} | + +{% if truncated %} +> **Внимание:** список находок обрезан из-за превышения лимита. Отчёт может быть неполным. +{% endif %} + +## Сводка + +| Критичность | Кол-во | +|---|---| +| P0 — Критические | {{ summary.get("P0", 0) }} | +| P1 — Высокие | {{ summary.get("P1", 0) }} | +| P2 — Средние | {{ summary.get("P2", 0) }} | +| **Итого** | **{{ summary.get("P0", 0) + summary.get("P1", 0) + summary.get("P2", 0) }}** | + +## Находки + +{% for severity, title in [("P0", "Критические"), ("P1", "Высокие"), ("P2", "Средние")] %} +### {{ severity }} — {{ title }} + +{% for f in grouped.get(severity, []) %} +**Находка {{ loop.index }}** + +| Поле | Значение | +|---|---| +| Правило | `{{ f.rule_id }}` | +| Файл | `{{ f.file_path }}` | +| Строка | {{ f.line_no or "—" }} | +| Секрет (маска) | `{{ f.masked_value }}` | +| Уверенность | {{ f.confidence }} | +| Источник | {{ f.source }} | +| Статус | {{ f.status }} | +| Коммит | `{{ f.commit_sha }}` | +{% if f.context %} +| Контекст | `{{ f.context }}` | +{% endif %} + +{% else %} +Находок не обнаружено. + +{% endfor %} +{% endfor %} +--- + +*Отчёт сформирован сервисом [ZabGU DevSecOps Hub](https://github.com/mshqq/ZabGU-DevSecOps-Hub). Секреты приведены в маскированном виде.*