diff --git a/plugins/magpie-security-tools/tools/asf-security-dash/README.md b/plugins/magpie-security-tools/tools/asf-security-dash/README.md index d1eef550f..2daaeb597 100644 --- a/plugins/magpie-security-tools/tools/asf-security-dash/README.md +++ b/plugins/magpie-security-tools/tools/asf-security-dash/README.md @@ -90,8 +90,7 @@ a PMC the user may not see, or a malformed response), `2` a usage error, | Field | Meaning | |---|---| | `message_id` | `Message-ID` of the reporting email, without angle brackets. The key for every comparison. | -| `tag` | The security team's name for the case (its file name), when the API returns it; else `null`. | -| `label` | `true` for an entry of one of the security team's standing labels rather than a report: a `tag` starting `aaa-` or `zzz-` (catch-all labels such as `aaa-support`, `zzz-admin`), or the `glasswing` / `non-fwd` states of the multi-thread labels. | +| `label` | `true` for an entry of one of the security team's multi-thread labels rather than a report: the `glasswing` / `non-fwd` states. | | `cves` | CVE IDs the security team has allocated for the case. | | `title` | Subject of the reporting email. | | `state` | `untriaged`, `confirmed`, or what the case waits for: `cve-allocation`, `disclosure`, `non-issue-feedback`, `non-issue-docs`, `non-issue-upstream`, or another `wf` state. | @@ -118,7 +117,7 @@ Each finding has `kind`, `fix`, `detail`, and the `case` and/or `issue` it conce | `kind` | `fix` | Meaning | |---|---|---| | `untracked` | `import` | The dashboard has a report the tracker has no issue for. | -| `untracked` | `review` | The same, for an untriaged case whose `tag` the API did not return: it may be a standing label, so a human checks the thread first. | +| `untracked` | `review` | The same, for an untriaged case: the dashboard also lists the security team's catch-all labels (`aaa-support`, `zzz-admin`, …) as untriaged cases, and the API does not mark them, so a human checks the thread first. | | `ambiguous` | `tracker` | The case matches more than one issue; record its `Message-ID` on the right one. Those issues get no `not-on-dashboard` finding meanwhile. | | `not-on-dashboard` | `asf-security` | An open issue with a security-list thread has no open case. | | `cve-missing-in-tracker` | `tracker` | The dashboard records a CVE the issue does not. | diff --git a/plugins/magpie-security-tools/tools/asf-security-dash/src/magpie_asf_security_dash/cases.py b/plugins/magpie-security-tools/tools/asf-security-dash/src/magpie_asf_security_dash/cases.py index ee2fb5c3e..df4a5ae30 100644 --- a/plugins/magpie-security-tools/tools/asf-security-dash/src/magpie_asf_security_dash/cases.py +++ b/plugins/magpie-security-tools/tools/asf-security-dash/src/magpie_asf_security_dash/cases.py @@ -41,9 +41,6 @@ #: States the dashboard gives the entries of its multi-thread labels #: (`aaa-glasswing`, `aaa-non-fwd`): the security team's own bookkeeping. LABEL_STATES = frozenset({"glasswing", "non-fwd"}) -#: Prefixes of the security team's standing labels: catch-all files such as -#: `aaa-support` or `zzz-admin`, which the dashboard lists as untriaged cases. -LABEL_PREFIXES = ("aaa-", "zzz-") DECISIONS = ("undecided", "accepted", "rejected") @@ -76,13 +73,10 @@ def normalise(raw: Iterable[Mapping[str, Any]]) -> list[dict[str, Any]]: cves = entry.get("cves") or [] if not isinstance(cves, list): raise DashError(f"case {mid}: cves is not a list") - tag = entry.get("tag") - tag = tag if isinstance(tag, str) and tag else None cases.append( { "message_id": mid, - "tag": tag, - "label": state in LABEL_STATES or bool(tag and tag.startswith(LABEL_PREFIXES)), + "label": state in LABEL_STATES, "cves": sorted({str(c).upper() for c in cves}), "title": str(entry.get("title") or ""), "state": state, @@ -119,7 +113,7 @@ def _finding( ) -> dict[str, Any]: found: dict[str, Any] = {"kind": kind, "fix": fix, "detail": detail} if case is not None: - found["case"] = {k: case[k] for k in ("message_id", "tag", "title", "state", "cves", "thread")} + found["case"] = {k: case[k] for k in ("message_id", "title", "state", "cves", "thread")} if issue is not None: found["issue"] = {k: issue[k] for k in ("id", "url", "state", "decision", "cves")} return found @@ -204,11 +198,11 @@ def reconcile(pmc: str, cases: list[dict[str, Any]], tracker: Iterable[Mapping[s def _untracked(case: dict[str, Any]) -> dict[str, Any]: """A case with no issue: import it, unless it may be one of the standing labels. - Without its `tag` (the dashboard's file name) a standing label such as - `aaa-support` is indistinguishable from an untriaged report, so a human - looks before anything is imported. + The dashboard lists the security team's catch-all labels, such as + `aaa-support`, as untriaged cases, and the API does not say which entries + they are, so a human looks before an untriaged case is imported. """ - if case["tag"] is None and case["stage"] == "untriaged": + if case["stage"] == "untriaged": return _finding( "untracked", "review", diff --git a/tools/asf-security-dash/README.md b/tools/asf-security-dash/README.md index 7819736e3..f5c1d1715 100644 --- a/tools/asf-security-dash/README.md +++ b/tools/asf-security-dash/README.md @@ -90,8 +90,7 @@ a PMC the user may not see, or a malformed response), `2` a usage error, | Field | Meaning | |---|---| | `message_id` | `Message-ID` of the reporting email, without angle brackets. The key for every comparison. | -| `tag` | The security team's name for the case (its file name), when the API returns it; else `null`. | -| `label` | `true` for an entry of one of the security team's standing labels rather than a report: a `tag` starting `aaa-` or `zzz-` (catch-all labels such as `aaa-support`, `zzz-admin`), or the `glasswing` / `non-fwd` states of the multi-thread labels. | +| `label` | `true` for an entry of one of the security team's multi-thread labels rather than a report: the `glasswing` / `non-fwd` states. | | `cves` | CVE IDs the security team has allocated for the case. | | `title` | Subject of the reporting email. | | `state` | `untriaged`, `confirmed`, or what the case waits for: `cve-allocation`, `disclosure`, `non-issue-feedback`, `non-issue-docs`, `non-issue-upstream`, or another `wf` state. | @@ -118,7 +117,7 @@ Each finding has `kind`, `fix`, `detail`, and the `case` and/or `issue` it conce | `kind` | `fix` | Meaning | |---|---|---| | `untracked` | `import` | The dashboard has a report the tracker has no issue for. | -| `untracked` | `review` | The same, for an untriaged case whose `tag` the API did not return: it may be a standing label, so a human checks the thread first. | +| `untracked` | `review` | The same, for an untriaged case: the dashboard also lists the security team's catch-all labels (`aaa-support`, `zzz-admin`, …) as untriaged cases, and the API does not mark them, so a human checks the thread first. | | `ambiguous` | `tracker` | The case matches more than one issue; record its `Message-ID` on the right one. Those issues get no `not-on-dashboard` finding meanwhile. | | `not-on-dashboard` | `asf-security` | An open issue with a security-list thread has no open case. | | `cve-missing-in-tracker` | `tracker` | The dashboard records a CVE the issue does not. | diff --git a/tools/asf-security-dash/src/magpie_asf_security_dash/cases.py b/tools/asf-security-dash/src/magpie_asf_security_dash/cases.py index ee2fb5c3e..df4a5ae30 100644 --- a/tools/asf-security-dash/src/magpie_asf_security_dash/cases.py +++ b/tools/asf-security-dash/src/magpie_asf_security_dash/cases.py @@ -41,9 +41,6 @@ #: States the dashboard gives the entries of its multi-thread labels #: (`aaa-glasswing`, `aaa-non-fwd`): the security team's own bookkeeping. LABEL_STATES = frozenset({"glasswing", "non-fwd"}) -#: Prefixes of the security team's standing labels: catch-all files such as -#: `aaa-support` or `zzz-admin`, which the dashboard lists as untriaged cases. -LABEL_PREFIXES = ("aaa-", "zzz-") DECISIONS = ("undecided", "accepted", "rejected") @@ -76,13 +73,10 @@ def normalise(raw: Iterable[Mapping[str, Any]]) -> list[dict[str, Any]]: cves = entry.get("cves") or [] if not isinstance(cves, list): raise DashError(f"case {mid}: cves is not a list") - tag = entry.get("tag") - tag = tag if isinstance(tag, str) and tag else None cases.append( { "message_id": mid, - "tag": tag, - "label": state in LABEL_STATES or bool(tag and tag.startswith(LABEL_PREFIXES)), + "label": state in LABEL_STATES, "cves": sorted({str(c).upper() for c in cves}), "title": str(entry.get("title") or ""), "state": state, @@ -119,7 +113,7 @@ def _finding( ) -> dict[str, Any]: found: dict[str, Any] = {"kind": kind, "fix": fix, "detail": detail} if case is not None: - found["case"] = {k: case[k] for k in ("message_id", "tag", "title", "state", "cves", "thread")} + found["case"] = {k: case[k] for k in ("message_id", "title", "state", "cves", "thread")} if issue is not None: found["issue"] = {k: issue[k] for k in ("id", "url", "state", "decision", "cves")} return found @@ -204,11 +198,11 @@ def reconcile(pmc: str, cases: list[dict[str, Any]], tracker: Iterable[Mapping[s def _untracked(case: dict[str, Any]) -> dict[str, Any]: """A case with no issue: import it, unless it may be one of the standing labels. - Without its `tag` (the dashboard's file name) a standing label such as - `aaa-support` is indistinguishable from an untriaged report, so a human - looks before anything is imported. + The dashboard lists the security team's catch-all labels, such as + `aaa-support`, as untriaged cases, and the API does not say which entries + they are, so a human looks before an untriaged case is imported. """ - if case["tag"] is None and case["stage"] == "untriaged": + if case["stage"] == "untriaged": return _finding( "untracked", "review", diff --git a/tools/asf-security-dash/tests/test_asf_security_dash.py b/tools/asf-security-dash/tests/test_asf_security_dash.py index 795ea3a64..041ba2912 100644 --- a/tools/asf-security-dash/tests/test_asf_security_dash.py +++ b/tools/asf-security-dash/tests/test_asf_security_dash.py @@ -95,20 +95,22 @@ def run(dash: list[dict[str, Any]], tracker: list[dict[str, Any]]) -> dict[str, return cases.reconcile("foo", cases.normalise(dash), tracker) -def test_an_untagged_untriaged_case_with_no_issue_is_reviewed_not_imported() -> None: - """Without its tag it may be one of the security team's standing labels.""" +def test_an_untriaged_case_with_no_issue_is_reviewed_not_imported() -> None: + """It may be one of the security team's standing labels.""" result = run([case("a@x")], []) assert kinds(result) == ["untracked"] assert result["findings"][0]["fix"] == "review" assert "standing labels" in result["findings"][0]["detail"] -def test_a_tagged_report_with_no_issue_is_imported() -> None: +def test_an_untriaged_case_is_reviewed_even_if_the_api_returns_a_tag() -> None: + """The tag is the security team's internal name, not part of the API.""" result = run([{**case("a@x"), "tag": "2026-10-01 upload traversal"}], []) - assert result["findings"][0]["fix"] == "import" + assert result["findings"][0]["fix"] == "review" + assert "tag" not in result["findings"][0]["case"] -def test_an_accepted_case_with_no_issue_is_imported_even_without_a_tag() -> None: +def test_an_accepted_case_with_no_issue_is_imported() -> None: result = run([case("a@x", "confirmed", ["CVE-2026-1"])], []) assert result["findings"][0]["fix"] == "import" @@ -116,12 +118,10 @@ def test_an_accepted_case_with_no_issue_is_imported_even_without_a_tag() -> None @pytest.mark.parametrize( "entry", [ - {**case("a@x"), "tag": "aaa-support"}, - {**case("a@x"), "tag": "zzz-admin"}, case("a@x", "glasswing", ["CVE-2026-9"]), case("a@x", "non-fwd"), ], - ids=["aaa-label", "zzz-label", "glasswing", "not-forwarded"], + ids=["glasswing", "not-forwarded"], ) def test_standing_label_entries_are_skipped(entry: dict[str, Any]) -> None: result = run([entry], []) diff --git a/tools/skill-evals/evals/security-issue-sync/README.md b/tools/skill-evals/evals/security-issue-sync/README.md index 9d785606b..365885d19 100644 --- a/tools/skill-evals/evals/security-issue-sync/README.md +++ b/tools/skill-evals/evals/security-issue-sync/README.md @@ -17,7 +17,7 @@ Steps 0 (pre-flight), 1a–1c and 1e (data gathering), 1g (cve.org API check), 4 | 5b | The four judged pre-push gates | 2 | A private-scanner analyst credit fails the anonymise gate; a clean fix-released record passes all four | | 1d | Actionable signals in comments and mail | 6 | Affected-versions lower bound (widen / keep + backtick-wrap), title strip and its under-3-words guard, private-scanner credit (anonymise / public-credit exemption), reporter CVSS surfaced only | | D0 | Dashboard mode is ASF-only | 3 | An ASF project proceeds; an `independent` project and an unset (`TODO`) organization stop without asking for a dashboard session | -| D4 | Dashboard-mode proposals | 3 | A dashboard-triage finding beside an untracked report; two findings for the ASF Security team; an untagged case that may be a standing label | +| D4 | Dashboard-mode proposals | 3 | A dashboard-triage finding beside an untracked report; two findings for the ASF Security team; an untriaged case that may be a standing label | | Hand-off delegate | Next-step hand-off honouring a project delegate (pre-flight `step-13`) | 4 | Triage delegated (`replace`) named and invoked through `security-issue-triage`, CVE allocation delegated (`after`), a delegate for an unrelated skill (not named), and `--no-overrides` (ignored) | ## Hard rules exercised diff --git a/tools/skill-evals/evals/security-issue-sync/step-d4-dashboard-proposals/fixtures/case-3-review-possible-label/report.md b/tools/skill-evals/evals/security-issue-sync/step-d4-dashboard-proposals/fixtures/case-3-review-possible-label/report.md index a25a33868..107bf9aa2 100644 --- a/tools/skill-evals/evals/security-issue-sync/step-d4-dashboard-proposals/fixtures/case-3-review-possible-label/report.md +++ b/tools/skill-evals/evals/security-issue-sync/step-d4-dashboard-proposals/fixtures/case-3-review-possible-label/report.md @@ -7,6 +7,6 @@ {"pmc": "foo", "cases": 1, "issues": 0, "paired": 0, "labels_skipped": 0, "findings": [ {"kind": "untracked", "fix": "review", "detail": "the tracker has no issue for this case; the dashboard does not say whether it is a report or one of the security team's standing labels (aaa-…, zzz-…), so check the thread before importing", - "case": {"message_id": "e5@example.org", "tag": null, "title": "Question about dependency versions", "state": "untriaged", "cves": [], "thread": "https://lists.apache.org/thread/e5%40example.org?"}} + "case": {"message_id": "e5@example.org", "title": "Question about dependency versions", "state": "untriaged", "cves": [], "thread": "https://lists.apache.org/thread/e5%40example.org?"}} ]} ``` diff --git a/tools/spec-loop/specs/adapters.md b/tools/spec-loop/specs/adapters.md index 291dcfb4b..6d5276395 100644 --- a/tools/spec-loop/specs/adapters.md +++ b/tools/spec-loop/specs/adapters.md @@ -87,10 +87,12 @@ by swapping the adapter, not the skill. reporting email's `Message-ID`; `reconcile --pmc P --tracker FILE` pairs them with tracker issues (by `Message-ID`, else a shared CVE) and lists each disagreement with who fixes it (`tracker`, `import`, `review`, - `dashboard-triage`, `asf-security`). Entries of the security team's standing - labels (`aaa-*` / `zzz-*` tags, `glasswing` / `non-fwd` states) are skipped; - while the API omits the tag, an untriaged untracked case is `review`, never - `import`. Auth is the ASF OAuth session cookie from + `dashboard-triage`, `asf-security`). Entries of the security team's + multi-thread labels (`glasswing` / `non-fwd` states) are skipped; the API does + not mark its catch-all labels (`aaa-*` / `zzz-*`), which it lists as untriaged + cases, so an untriaged untracked case is `review`, never `import`. The bridge + reads no `tag`: it is the security team's internal name and not part of the + API (apache/security-dash#62). Auth is the ASF OAuth session cookie from `~/.config/apache-magpie/asf-security-dash/session.json`, sent only to the dashboard's origin with GET and no redirects followed; `--input` reads API output saved from a signed-in browser instead. `security-issue-sync`'s