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
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")


Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 2 additions & 3 deletions tools/asf-security-dash/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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. |
Expand Down
18 changes: 6 additions & 12 deletions tools/asf-security-dash/src/magpie_asf_security_dash/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")


Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down
16 changes: 8 additions & 8 deletions tools/asf-security-dash/tests/test_asf_security_dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,33 +95,33 @@ 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"


@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], [])
Expand Down
2 changes: 1 addition & 1 deletion tools/skill-evals/evals/security-issue-sync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": "[email protected]", "tag": null, "title": "Question about dependency versions", "state": "untriaged", "cves": [], "thread": "https://lists.apache.org/thread/e5%40example.org?<security.apache.org>"}}
"case": {"message_id": "[email protected]", "title": "Question about dependency versions", "state": "untriaged", "cves": [], "thread": "https://lists.apache.org/thread/e5%40example.org?<security.apache.org>"}}
]}
```
10 changes: 6 additions & 4 deletions tools/spec-loop/specs/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading