You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@security.apache.org by pk...@apache.org on 2026/09/29 15:34:44 UTC
(security-dash) 01/01: feat: summarize incubating projects on the incubator page
This is an automated email from the ASF dual-hosted git repository.
ppkarwasz pushed a commit to branch feat/incubator-summary
in repository https://gitbox.apache.org/repos/asf/security-dash.git
commit 70be9d180a347118b97d27394b94470961e59e88
Author: Piotr P. Karwasz <pi...@github.copernik.eu>
AuthorDate: Tue Sep 29 17:34:29 2026 +0200
feat: summarize incubating projects on the incubator page
All security reports for incubating projects are assigned to the podlings,
so the `incubator` project page was always empty.
The incubator page now shows:
- A sortable summary table listing, for each podling, the number of
untriaged and open reports and the date of the oldest untriaged report.
It is initially sorted by untriaged, then total, reports.
- The reports of each podling, grouped by state as on the project page.
The podlings are configured with the new `pmcs_in_incubator` setting.
Fixes #29
Assisted-By: Claude Opus 5.5 <no...@anthropic.com>
---
app/__init__.py | 32 +++++++++++++---
app/config.py | 3 ++
app/static/assets/css/app.css | 48 ++++++++++++++++++++++++
app/static/assets/js/sortable-table.js | 68 ++++++++++++++++++++++++++++++++++
app/templates/includes/reports.html | 51 +++++++++++++++++++++++++
app/templates/incubator.html | 64 ++++++++++++++++++++++++++++++++
app/templates/project.html | 50 +------------------------
config.yaml.example | 3 ++
8 files changed, 264 insertions(+), 55 deletions(-)
diff --git a/app/__init__.py b/app/__init__.py
index 53a874c..1aa6fad 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -152,20 +152,40 @@ async def _audit_access(project: str):
print(f"User {user.uid} accessed project {project}{mark}")
+def _sections(r: list[reports.Report]) -> list[tuple[str, str, str, list[reports.Report]]]:
+ states = sorted(dict.fromkeys(report.state for report in r), key=_state_sort_key)
+ return [
+ (state, _state_title(state), _state_description(state), [report for report in r if report.state == state])
+ for state in states
+ ]
+
+async def _incubator(podlings: list[str]):
+ podling_sections = {podling: _sections(await reports.load_pmc_reports(podling)) for podling in podlings}
+ untriaged = {
+ podling: [report for state, _, _, r in sections if state == "untriaged" for report in r]
+ for podling, sections in podling_sections.items()
+ }
+ return await quart.render_template("incubator.html",
+ untriaged_description=_state_description("untriaged"),
+ summary=sorted(
+ ((podling, len(r), sum(len(s) for _, _, _, s in podling_sections[podling]),
+ min((report.date for report in r), default=None))
+ for podling, r in untriaged.items()),
+ key=lambda row: (-row[1], -row[2], row[0])),
+ podling_sections=podling_sections,
+ pmcs_with_subprojects=config.get().pmcs_with_subprojects)
+
@CLIENT.route("/project/<project>")
async def project(project: str):
await _require_authorization_for(project)
await _audit_access(project)
+ if project == "incubator":
+ return await _incubator(config.get().pmcs_in_incubator)
r = await reports.load_pmc_reports(project)
- states = sorted(dict.fromkeys(report.state for report in r), key=_state_sort_key)
- sections = [
- (_state_title(state), _state_description(state), [report for report in r if report.state == state])
- for state in states
- ]
return await quart.render_template("project.html",
project_name=project,
debt_constant=statistics.DEBT_CONSTANT,
- sections=sections,
+ sections=_sections(r),
show_subproject=project in config.get().pmcs_with_subprojects)
@CLIENT.route("/api/project/<project>/reports")
diff --git a/app/config.py b/app/config.py
index a82e830..d6b04aa 100644
--- a/app/config.py
+++ b/app/config.py
@@ -53,6 +53,9 @@ class AppConfig(pydantic.BaseModel):
pmcs_with_subprojects: list[str] = []
"""PMCs whose reports are split across subprojects."""
+ pmcs_in_incubator: list[str] = []
+ """Podlings in the Incubator, summarized on the incubator page."""
+
server: ServerConfig = ServerConfig()
@property
diff --git a/app/static/assets/css/app.css b/app/static/assets/css/app.css
index 42576c0..7407e1c 100644
--- a/app/static/assets/css/app.css
+++ b/app/static/assets/css/app.css
@@ -188,3 +188,51 @@ footer p {
.congrats {
font-size: 2em;
}
+
+.incubator-table {
+ width: auto;
+ border: 1px solid #d1d2d3;
+}
+
+.incubator-table th,
+.incubator-table td {
+ border: 1px solid #d1d2d3;
+ padding: 0.25rem 0.75rem;
+ text-align: left;
+}
+
+.incubator-table thead th {
+ background-color: #eeeeee;
+}
+
+.incubator-table tbody tr:nth-child(even) {
+ background-color: #f8f9fa;
+}
+
+.incubator-table .incubator-count {
+ text-align: right;
+}
+
+.sortable-table th button {
+ background: none;
+ border: none;
+ padding: 0;
+ color: inherit;
+ font: inherit;
+ cursor: pointer;
+}
+
+.sortable-table th button::after {
+ content: " \2195";
+ color: #999999;
+}
+
+.sortable-table th[aria-sort="ascending"] button::after {
+ content: " \25B2";
+ color: inherit;
+}
+
+.sortable-table th[aria-sort="descending"] button::after {
+ content: " \25BC";
+ color: inherit;
+}
diff --git a/app/static/assets/js/sortable-table.js b/app/static/assets/js/sortable-table.js
new file mode 100644
index 0000000..a67ddaa
--- /dev/null
+++ b/app/static/assets/js/sortable-table.js
@@ -0,0 +1,68 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// Sorts the rows of `table.sortable-table` when a header button is clicked.
+// A cell sorts by its `data-sort-value`, or else its text; a header with
+// `data-sort-type="number"` sorts numerically. Empty cells always sort last.
+// The current order is exposed to assistive technology through `aria-sort`.
+(function () {
+ "use strict";
+
+ function sortValue(row, index) {
+ var cell = row.cells[index];
+ var value = cell.dataset.sortValue;
+ return (value !== undefined ? value : cell.textContent).trim();
+ }
+
+ function sortBy(table, th) {
+ var headers = th.parentElement.children;
+ var index = Array.prototype.indexOf.call(headers, th);
+ var numeric = th.dataset.sortType === "number";
+ var ascending = th.getAttribute("aria-sort") !== "ascending";
+ var tbody = table.tBodies[0];
+
+ var rows = Array.prototype.slice.call(tbody.rows);
+ rows.sort(function (a, b) {
+ var x = sortValue(a, index);
+ var y = sortValue(b, index);
+ if (x === "" || y === "") {
+ return (x === "") - (y === "");
+ }
+ var order = numeric ? Number(x) - Number(y) : x.localeCompare(y);
+ return ascending ? order : -order;
+ });
+ rows.forEach(function (row) {
+ tbody.appendChild(row);
+ });
+
+ Array.prototype.forEach.call(headers, function (header) {
+ header.removeAttribute("aria-sort");
+ });
+ th.setAttribute("aria-sort", ascending ? "ascending" : "descending");
+ }
+
+ document.querySelectorAll("table.sortable-table").forEach(function (table) {
+ table.querySelectorAll("thead th").forEach(function (th) {
+ var button = th.querySelector("button");
+ if (button) {
+ button.addEventListener("click", function () {
+ sortBy(table, th);
+ });
+ }
+ });
+ });
+})();
diff --git a/app/templates/includes/reports.html b/app/templates/includes/reports.html
new file mode 100644
index 0000000..8a79d6e
--- /dev/null
+++ b/app/templates/includes/reports.html
@@ -0,0 +1,51 @@
+{#- Sections of reports grouped by state.
+ Expects: sections, project_name, show_subproject and optionally anchor_prefix. -#}
+{% for state, title, description, reports in sections %}
+ <h3 id="{{ anchor_prefix|default("") }}{{ state }}">{{ title }}
+ {% if reports|length > 5 %}
+ ({{ reports|length }})
+ {% endif %}
+ </h3>
+ {% if description %}
+ <p>{{ description }}</p>
+ {% endif %}
+ <table class="reports-table">
+ <tbody>
+ {% for report in reports|sort(attribute='date') %}
+ <tr>
+ <!-- {{ report.security_team_name }} -->
+ <td class="reports-date">{{ report.date.strftime('%Y-%m-%d') }}</td>
+ {% if show_subproject %}
+ <td class="reports-subproject">{{ report.subproject or "" }}</td>
+ {% endif %}
+ <td class="reports-jira">
+ {%- if report.github -%}
+ <a href="{{report.github[1]}}">{{ report.github[0] }}</a>
+ {%- endif -%}
+ {%- if report.jira -%}
+ <a href="https://issues.apache.org/jira/browse/{{report.jira}}">#{{ report.jira.split('-')[-1] }}</a>
+ {%- endif -%}
+ </td>
+ <td class="reports-cves">
+ {%- for cve in report.cves -%}
+ <div><a href="https://cveprocess.apache.org/cve5/{{cve}}">{{ cve }}</a></div>
+ {%- endfor -%}
+ {%- if report.state == 'confirmed' and not report.cves -%}
+ <div><a href="https://cveprocess.apache.org/allocatecve?pmc={{ project_name|urlencode }}&title={{ report.sanitized_title|urlencode }}">allocate CVE</a></div>
+ {%- endif -%}
+ </td>
+ <td class="reports-reporter">
+ {% if report.reporter %}
+ <span class="reporter-initials"
+ style="background-color: {{ report.reporter.color }};"
+ title="{{ report.reporter.tooltip }}">{{ report.reporter.initials }}</span>
+ {% endif %}
+ </td>
+ <td class="reports-title">
+ <a href="{{ report.link }}" title="{{ report.title }}">{{ report.title }}</a>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+{% endfor %}
diff --git a/app/templates/incubator.html b/app/templates/incubator.html
new file mode 100644
index 0000000..a350dce
--- /dev/null
+++ b/app/templates/incubator.html
@@ -0,0 +1,64 @@
+{% extends "layouts/base.html" %}
+
+{% block title %}
+Currently open security reports for incubating projects
+{% endblock title %}
+{% block page_title %}
+Currently open security reports for incubating projects
+{% endblock page_title %}
+
+{% block content %}
+<p>
+This dashboard gives an overview of the security reports currently tracked by the Apache Security Team for the podlings of the <b>incubator</b>.
+</p>
+
+<p>
+Information on this page may be delayed by as much as a few days.
+If you see anything that seems incorrect, email
+<a href="mailto:security@apache.org">security@apache.org</a>
+</p>
+
+<h2>Summary</h2>
+<table class="reports-table incubator-table sortable-table">
+ <thead>
+ <tr>
+ <th><button type="button">Podling</button></th>
+ <th aria-sort="descending" data-sort-type="number" title="{{ untriaged_description }}"><button type="button">Untriaged</button></th>
+ <th data-sort-type="number"><button type="button">Total</button></th>
+ <th><button type="button">Oldest untriaged</button></th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for podling, untriaged, total, oldest in summary %}
+ <tr>
+ <td><a href="#{{ podling }}">{{ podling }}</a></td>
+ <td class="incubator-count" data-sort-value="{{ untriaged }}">
+ {%- if untriaged -%}
+ <a href="#{{ podling }}-untriaged">{{ untriaged }}</a>
+ {%- endif -%}
+ </td>
+ <td class="incubator-count">{{ total }}</td>
+ <td>{{ oldest.strftime('%Y-%m-%d') if oldest else "" }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+
+{% for podling, sections in podling_sections|dictsort %}
+ <h2 id="{{ podling }}">{{ podling }}</h2>
+ {% if sections %}
+ {% with project_name=podling,
+ show_subproject=podling in pmcs_with_subprojects,
+ anchor_prefix=podling ~ "-" %}
+ {% include "includes/reports.html" %}
+ {% endwith %}
+ {% else %}
+ <p>No open security reports.</p>
+ {% endif %}
+{% endfor %}
+{% endblock content %}
+
+{% block javascripts %}
+ {{ super() }}
+ <script src="/assets/js/sortable-table.js"></script>
+{% endblock javascripts %}
diff --git a/app/templates/project.html b/app/templates/project.html
index 8b4007f..8d89965 100644
--- a/app/templates/project.html
+++ b/app/templates/project.html
@@ -31,55 +31,7 @@ If you see anything that seems incorrect, email
<h2>Reports</h2>
{% if sections %}
- {% for title, description, reports in sections %}
- <h3>{{ title }}
- {% if reports|length > 5 %}
- ({{ reports|length }})
- {% endif %}
- </h3>
- {% if description %}
- <p>{{ description }}</p>
- {% endif %}
- <table class="reports-table">
- <tbody>
- {% for report in reports|sort(attribute='date') %}
- <tr>
- <!-- {{ report.security_team_name }} -->
- <td class="reports-date">{{ report.date.strftime('%Y-%m-%d') }}</td>
- {% if show_subproject %}
- <td class="reports-subproject">{{ report.subproject or "" }}</td>
- {% endif %}
- <td class="reports-jira">
- {%- if report.github -%}
- <a href="{{report.github[1]}}">{{ report.github[0] }}</a>
- {%- endif -%}
- {%- if report.jira -%}
- <a href="https://issues.apache.org/jira/browse/{{report.jira}}">#{{ report.jira.split('-')[-1] }}</a>
- {%- endif -%}
- </td>
- <td class="reports-cves">
- {%- for cve in report.cves -%}
- <div><a href="https://cveprocess.apache.org/cve5/{{cve}}">{{ cve }}</a></div>
- {%- endfor -%}
- {%- if report.state == 'confirmed' and not report.cves -%}
- <div><a href="https://cveprocess.apache.org/allocatecve?pmc={{ project_name|urlencode }}&title={{ report.sanitized_title|urlencode }}">allocate CVE</a></div>
- {%- endif -%}
- </td>
- <td class="reports-reporter">
- {% if report.reporter %}
- <span class="reporter-initials"
- style="background-color: {{ report.reporter.color }};"
- title="{{ report.reporter.tooltip }}">{{ report.reporter.initials }}</span>
- {% endif %}
- </td>
- <td class="reports-title">
- <a href="{{ report.link }}" title="{{ report.title }}">{{ report.title }}</a>
- </td>
- </tr>
- {% endfor %}
- </tbody>
- </table>
- {% endfor %}
+ {% include "includes/reports.html" %}
{% else %}
<center>
<p class="congrats">Congratulations! Your project is back to 0 open security reports!</p>
diff --git a/config.yaml.example b/config.yaml.example
index d16533e..d4ee7c8 100644
--- a/config.yaml.example
+++ b/config.yaml.example
@@ -6,5 +6,8 @@ pmcs_using_jira:
pmcs_with_security_emails:
- airflow
- ambari
+pmcs_in_incubator:
+ - amoro
+ - seata
pmcs_with_subprojects:
- commons