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/08/26 10:12:05 UTC
(security-dash) 01/01: feat: show attic project reports on the security dashboard
This is an automated email from the ASF dual-hosted git repository.
ppkarwasz pushed a commit to branch attic-reports
in repository https://gitbox.apache.org/repos/asf/security-dash.git
commit e8bf3efcbc087c407f13d0e0df3682b84dc6719f
Author: Piotr P. Karwasz <pi...@github.copernik.eu>
AuthorDate: Wed Aug 26 12:11:50 2026 +0200
feat: show attic project reports on the security dashboard
Projects retired to the Attic no longer have a PMC to handle their
security reports, so the security team handles them directly. The
'security' project view now aggregates, in addition to its own
reports, the reports of every PMC listed in the new 'attic_pmcs'
config option, labeling each with its originating PMC via the
subproject column.
Assisted-By: Claude Fable 5 <no...@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RZ9HrGxceN3MFigykAifKi
---
app/__init__.py | 2 +-
app/config.py | 3 +++
app/reports.py | 22 +++++++++++++++++----
app/test_reports.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
config.yaml.example | 3 +++
5 files changed, 81 insertions(+), 6 deletions(-)
diff --git a/app/__init__.py b/app/__init__.py
index 53a874c..c4e13b9 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -166,7 +166,7 @@ async def project(project: str):
project_name=project,
debt_constant=statistics.DEBT_CONSTANT,
sections=sections,
- show_subproject=project in config.get().pmcs_with_subprojects)
+ show_subproject=project in config.get().pmcs_with_subprojects or project == "security")
@CLIENT.route("/api/project/<project>/reports")
async def project_reports_api(project: str):
diff --git a/app/config.py b/app/config.py
index a82e830..89537c1 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."""
+ attic_pmcs: list[str] = []
+ """PMCs retired to the Attic, whose reports the security team handles directly"""
+
server: ServerConfig = ServerConfig()
@property
diff --git a/app/reports.py b/app/reports.py
index 35688f6..8454776 100644
--- a/app/reports.py
+++ b/app/reports.py
@@ -210,11 +210,25 @@ def load_pmc_report(pmc: str, path: pathlib.Path) -> Report | None:
datetime.datetime.fromtimestamp(first_email['mailtime'], tz=datetime.timezone.utc),
)
-async def load_pmc_reports(pmc: str) -> list[Report]:
- if not re.fullmatch(r"[a-z0-9]+", pmc):
- raise ValueError(f"invalid PMC name: {pmc!r}")
-
+def _load_reports_dir(pmc: str) -> list[Report]:
d = config.get().data_dir_path / pmc
threads = list(d.glob('**/*.json'))
return [ r for r in (load_pmc_report(pmc, t) for t in threads) if r is not None ]
+
+async def load_pmc_reports(pmc: str) -> list[Report]:
+ if not re.fullmatch(r"[a-z0-9]+", pmc):
+ raise ValueError(f"invalid PMC name: {pmc!r}")
+
+ result = _load_reports_dir(pmc)
+ # attic projects no longer have a PMC, so the security team
+ # handles their reports directly
+ if pmc == "security":
+ for attic_pmc in config.get().attic_pmcs:
+ if not re.fullmatch(r"[a-z0-9]+", attic_pmc):
+ continue
+ result.extend(
+ dataclasses.replace(r, subproject=attic_pmc)
+ for r in _load_reports_dir(attic_pmc)
+ )
+ return result
diff --git a/app/test_reports.py b/app/test_reports.py
index d7816a8..81768c9 100644
--- a/app/test_reports.py
+++ b/app/test_reports.py
@@ -1,8 +1,9 @@
+import asyncio
import json
import types
from app import reports
-from app.reports import Reporter, _asf_member_link, _project_link, _reporter, load_pmc_report
+from app.reports import Reporter, _asf_member_link, _project_link, _reporter, load_pmc_report, load_pmc_reports
def _write_report(path, label, *, subj="[SECURITY] a flaw"):
@@ -55,6 +56,60 @@ def test_subproject_none_when_no_prefix(tmp_path, monkeypatch):
assert report.subproject is None
+def _full_config(tmp_path, attic_pmcs=()):
+ return types.SimpleNamespace(
+ data_dir_path=tmp_path,
+ attic_pmcs=list(attic_pmcs),
+ pmcs_using_jira={},
+ pmcs_using_github={},
+ )
+
+
+def test_security_pmc_includes_attic_reports(tmp_path, monkeypatch):
+ monkeypatch.setattr(
+ reports.config, "get", lambda: _full_config(tmp_path, attic_pmcs=["hivemind"])
+ )
+ (tmp_path / "security").mkdir()
+ (tmp_path / "hivemind").mkdir()
+ _write_report(tmp_path / "security" / "x", "own report.json")
+ _write_report(tmp_path / "hivemind" / "x", "attic report.json")
+
+ result = asyncio.run(load_pmc_reports("security"))
+
+ assert {r.security_team_name for r in result} == {"own report.json", "attic report.json"}
+ by_name = {r.security_team_name: r for r in result}
+ assert by_name["attic report.json"].subproject == "hivemind"
+ assert by_name["own report.json"].subproject is None
+
+
+def test_ordinary_pmc_does_not_include_attic_reports(tmp_path, monkeypatch):
+ monkeypatch.setattr(
+ reports.config, "get", lambda: _full_config(tmp_path, attic_pmcs=["hivemind"])
+ )
+ (tmp_path / "cassandra").mkdir()
+ (tmp_path / "hivemind").mkdir()
+ _write_report(tmp_path / "cassandra" / "x", "own report.json")
+ _write_report(tmp_path / "hivemind" / "x", "attic report.json")
+
+ result = asyncio.run(load_pmc_reports("cassandra"))
+
+ assert {r.security_team_name for r in result} == {"own report.json"}
+
+
+def test_security_pmc_tolerates_missing_and_invalid_attic_dirs(tmp_path, monkeypatch):
+ monkeypatch.setattr(
+ reports.config,
+ "get",
+ lambda: _full_config(tmp_path, attic_pmcs=["hivemind", "../escape"]),
+ )
+ (tmp_path / "security").mkdir()
+ _write_report(tmp_path / "security" / "x", "own report.json")
+
+ result = asyncio.run(load_pmc_reports("security"))
+
+ assert {r.security_team_name for r in result} == {"own report.json"}
+
+
def test_asf_member_link_non_apache_to_uses_security_apache_org():
email = {
'to': 'Disclosure <di...@aisle.com>',
diff --git a/config.yaml.example b/config.yaml.example
index d16533e..205c1fd 100644
--- a/config.yaml.example
+++ b/config.yaml.example
@@ -8,3 +8,6 @@ pmcs_with_security_emails:
- ambari
pmcs_with_subprojects:
- commons
+attic_pmcs:
+ - hivemind
+ - shale