Skip to content
Merged
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
79 changes: 52 additions & 27 deletions app/routes/scan.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -14,10 +14,10 @@
@scan_bp.route("/api/scans/<int:scan_id>", 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(
{
Expand All @@ -32,20 +32,28 @@ def scan_status(scan_id):
), 200


@scan_bp.route("/api/scans/<int:scan_id>/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()
Expand 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/<int:scan_id>/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,
Expand All @@ -91,3 +96,23 @@ def group_sort_severity(findings):
"findings": grouped,
}
)


@scan_bp.route("/api/scans/<int:scan_id>/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)
53 changes: 53 additions & 0 deletions app/templates/reports/scan_report.md
Original file line number Diff line number Diff line change
@@ -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). Секреты приведены в маскированном виде.*