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
11 changes: 7 additions & 4 deletions plugins/magpie-security-tools/tools/asf-security-dash/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,18 @@ A project without a tracker that wants Magpie to keep per-report state pairs it
## Invocation

```bash
uv run --project tools/asf-security-dash magpie-asf-security-dash cases --pmc <project>
uv run --project tools/asf-security-dash magpie-asf-security-dash reconcile --pmc <project> --tracker issues.json
uv run --project tools/asf-security-dash magpie-asf-security-dash cases --pmc <project> --project-md <project-config>/project.md
uv run --project tools/asf-security-dash magpie-asf-security-dash reconcile --pmc <project> --project-md <project-config>/project.md --tracker issues.json
# Either command reads saved API output instead of fetching it:
uv run --project tools/asf-security-dash magpie-asf-security-dash cases --pmc <project> --input reports.json
uv run --project tools/asf-security-dash magpie-asf-security-dash cases --pmc <project> --project-md <project-config>/project.md --input reports.json
```

`--pmc` is the PMC id as the dashboard spells it (`project_url` and the API path use it as is).
`--project-md` is the project's resolved `project.md`: the dashboard covers ASF projects only,
so the bridge refuses, before it reads a session or sends a request, unless that file declares `organization: ASF`.
Exit codes: `0` success, `1` an error (the message names a missing or expired session,
a PMC the user may not see, or a malformed response), `2` a usage error.
a PMC the user may not see, or a malformed response), `2` a usage error,
`3` the project is not an ASF project.

## Output contract

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@
def main(argv: Sequence[str] | None = None) -> int:
"""CLI entry point."""
from magpie_asf_security_dash.cli import main as cli_main
from magpie_asf_security_dash.client import DashError
from magpie_asf_security_dash.client import DashError, NotAsf

try:
return cli_main(argv)
except NotAsf as exc:
print(f"magpie-asf-security-dash: {exc}", file=sys.stderr)
return 3
except DashError as exc:
print(f"magpie-asf-security-dash error: {exc}", file=sys.stderr)
return 1
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,31 @@
from typing import Any

from magpie_asf_security_dash import cases
from magpie_asf_security_dash.client import DashError, check_pmc, fetch_reports, load_cookie, parse_reports
from magpie_asf_security_dash.client import (
DashError,
NotAsf,
check_pmc,
fetch_reports,
load_cookie,
parse_reports,
)
from magpie_asf_security_dash.organization import read_organization


def main(argv: Sequence[str] | None = None) -> int:
"""Run the magpie-asf-security-dash command line interface."""
args = _build_parser().parse_args(argv)
# dash.security.apache.org lists ASF PMCs only: nothing is read or sent
# for a project that does not declare `organization: ASF`.
try:
org = read_organization(args.project_md)
except OSError as exc:
raise DashError(f"cannot read {args.project_md}: {exc}") from exc
if org != "ASF":
raise NotAsf(
f"{args.project_md} declares organization {org!r}; the ASF Security dashboard "
"covers ASF projects only (organization: ASF)"
)
pmc = check_pmc(args.pmc)
raw = parse_reports(args.input.read_bytes()) if args.input else fetch_reports(pmc, load_cookie())
found = cases.normalise(raw)
Expand Down Expand Up @@ -70,6 +89,13 @@ def _build_parser() -> argparse.ArgumentParser:
command.add_argument(
"--pmc", required=True, help="PMC id, as the dashboard spells it (e.g. airflow)."
)
command.add_argument(
"--project-md",
type=Path,
required=True,
help="The project's resolved <project-config>/project.md; the bridge runs only "
"when it declares organization: ASF.",
)
command.add_argument(
"--input",
type=Path,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class DashError(Exception):
"""Raised when the bridge cannot complete a request."""


class NotAsf(DashError):
"""The project is not an ASF project, so the dashboard has nothing for it."""


class SessionExpired(DashError):
"""The session cookie is missing, rejected, or has expired."""

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 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.

"""The project's organization, read from its `project.md`.

The dashboard lists ASF PMCs only, so the bridge runs only for a project
whose `project.md` declares `organization: ASF`. The reading mirrors
`release_config.mdconfig.organization` (a `| organization | ... |` row,
else an `organization:` line; `independent` when neither is set), kept
here so the bridge depends on nothing else in the framework.
"""

from __future__ import annotations

import re
from pathlib import Path

DEFAULT = "independent"
_FENCE = re.compile(r"^\s*(```|~~~)")
# A value is a backticked token (an annotation may follow it, as in
# `ASF` (derived: apache/ GitHub org)) or, unquoted, the whole cell / rest of
# the line: never a prefix, so `ASF.example` is not `ASF`.
_ROW = re.compile(r"^\s*\|\s*`?organization`?\s*\|([^|]*)(?:\||$)")
_LINE = re.compile(r"^\s*organization:(.*)$")
_QUOTED = re.compile(r"^`([^`]*)`")


def _value(raw: str) -> str:
raw = raw.strip()
quoted = _QUOTED.match(raw)
return (quoted.group(1) if quoted else raw).strip()


_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)


def organization(text: str) -> str:
"""`project.md` → `organization`; `independent` when unset or still a TODO."""
inside = False
line_value = None
# A commented-out row is not configuration (release_config strips them too).
for line in _COMMENT.sub("", text).splitlines():
if _FENCE.match(line):
inside = not inside
continue
if inside or line.lstrip().startswith(">"):
continue
row = _ROW.match(line)
if row:
value = _value(row.group(1))
return DEFAULT if not value or value.startswith("TODO") else value
if line_value is None and (match := _LINE.match(line)) and (value := _value(match.group(1))):
line_value = value
return line_value or DEFAULT


def read_organization(path: Path) -> str:
"""The organization `path` declares; raises `OSError` when it cannot be read."""
return organization(path.read_text(encoding="utf-8"))
6 changes: 3 additions & 3 deletions plugins/magpie-security/skills/issue-sync/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ when_to_use: |
the tracker with dash.security.apache.org.
argument-hint: "[issue-number]"
capability: capability:intake
surface_hash: sha256:019c545c3013d4c2
surface_hash: sha256:5d8871fa61317a06
license: Apache-2.0
measured_tokens: 7058
measured_tokens: 7105
---

<!-- Placeholder convention (see AGENTS.md#placeholder-convention-used-in-skill-files):
Expand Down Expand Up @@ -215,7 +215,7 @@ syntax, hard rules, when bulk mode is NOT appropriate — lives in

## Dashboard mode — reconciling with dash.security.apache.org

ASF projects only.
ASF projects only (`organization: ASF` in `<project-config>/project.md`): for any other organization the mode is not offered, and a request for it ends at [Step D0](dashboard.md#step-d0--asf-projects-only).
On *"sync dashboard"* or *"reconcile with the security dashboard"*,
compare `<tracker>` with the ASF Security team's case list for the PMC and propose a fix for each disagreement,
following [`dashboard.md`](dashboard.md).
Expand Down
17 changes: 15 additions & 2 deletions plugins/magpie-security/skills/issue-sync/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,29 @@ The bridge, its output and its credential are documented in

Triggers: *"sync dashboard"*, *"reconcile with the security dashboard"*, *"what does dash.security say"*.

## Step D0 — ASF projects only

Read `organization` from the resolved [`<project-config>/project.md`](../../../../<project-config>/project.md) (personal layer first).
Anything but `ASF` — `independent`, another organization, unset, or a `TODO` — ends this mode here:
say that the dashboard covers ASF PMCs only, so there is nothing to reconcile, and offer the regular sync instead.
When `organization` is unset or a `TODO` but `<upstream>` is under the `apache/` GitHub organization,
the project is an ASF project whose config does not say so yet:
propose setting it with `/magpie-setup:setup config` (it writes `organization: ASF`, derived from the `apache/` org), then re-run.
Never run the bridge, ask for a dashboard session or mention `dash.security.apache.org` for such a project.
The bridge enforces the same rule on its own: it refuses (exit `3`) unless `--project-md` declares `organization: ASF`.

## Step D1 — Read the dashboard's cases

`<pmc>` is the PMC id the dashboard uses: `cna_private_owner` from
[`<project-config>/project.md`](../../../../<project-config>/project.md#cve-tooling),
else `<project>`.

```bash
uv run --project <framework>/tools/asf-security-dash magpie-asf-security-dash cases --pmc <pmc>
uv run --project <framework>/tools/asf-security-dash magpie-asf-security-dash cases --pmc <pmc> --project-md <project-config>/project.md
```

Exit `3` means the project is not an ASF project after all (Step D0): stop the mode.

On a missing or expired session (exit 1, the message says so), do not ask for the cookie in chat.
Offer the user the two ways in the bridge's README:
save the session cookie to `~/.config/apache-magpie/asf-security-dash/session.json` themselves,
Expand All @@ -48,7 +61,7 @@ When a finding shows that, propose adding the dashboard's `Message-ID` to the is
## Step D3 — Compare

```bash
uv run --project <framework>/tools/asf-security-dash magpie-asf-security-dash reconcile --pmc <pmc> --tracker <file> [--input <saved reports>]
uv run --project <framework>/tools/asf-security-dash magpie-asf-security-dash reconcile --pmc <pmc> --project-md <project-config>/project.md --tracker <file> [--input <saved reports>]
```

## Step D4 — Propose
Expand Down
11 changes: 7 additions & 4 deletions tools/asf-security-dash/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,18 @@ A project without a tracker that wants Magpie to keep per-report state pairs it
## Invocation

```bash
uv run --project tools/asf-security-dash magpie-asf-security-dash cases --pmc <project>
uv run --project tools/asf-security-dash magpie-asf-security-dash reconcile --pmc <project> --tracker issues.json
uv run --project tools/asf-security-dash magpie-asf-security-dash cases --pmc <project> --project-md <project-config>/project.md
uv run --project tools/asf-security-dash magpie-asf-security-dash reconcile --pmc <project> --project-md <project-config>/project.md --tracker issues.json
# Either command reads saved API output instead of fetching it:
uv run --project tools/asf-security-dash magpie-asf-security-dash cases --pmc <project> --input reports.json
uv run --project tools/asf-security-dash magpie-asf-security-dash cases --pmc <project> --project-md <project-config>/project.md --input reports.json
```

`--pmc` is the PMC id as the dashboard spells it (`project_url` and the API path use it as is).
`--project-md` is the project's resolved `project.md`: the dashboard covers ASF projects only,
so the bridge refuses, before it reads a session or sends a request, unless that file declares `organization: ASF`.
Exit codes: `0` success, `1` an error (the message names a missing or expired session,
a PMC the user may not see, or a malformed response), `2` a usage error.
a PMC the user may not see, or a malformed response), `2` a usage error,
`3` the project is not an ASF project.

## Output contract

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@
def main(argv: Sequence[str] | None = None) -> int:
"""CLI entry point."""
from magpie_asf_security_dash.cli import main as cli_main
from magpie_asf_security_dash.client import DashError
from magpie_asf_security_dash.client import DashError, NotAsf

try:
return cli_main(argv)
except NotAsf as exc:
print(f"magpie-asf-security-dash: {exc}", file=sys.stderr)
return 3
except DashError as exc:
print(f"magpie-asf-security-dash error: {exc}", file=sys.stderr)
return 1
28 changes: 27 additions & 1 deletion tools/asf-security-dash/src/magpie_asf_security_dash/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,31 @@
from typing import Any

from magpie_asf_security_dash import cases
from magpie_asf_security_dash.client import DashError, check_pmc, fetch_reports, load_cookie, parse_reports
from magpie_asf_security_dash.client import (
DashError,
NotAsf,
check_pmc,
fetch_reports,
load_cookie,
parse_reports,
)
from magpie_asf_security_dash.organization import read_organization


def main(argv: Sequence[str] | None = None) -> int:
"""Run the magpie-asf-security-dash command line interface."""
args = _build_parser().parse_args(argv)
# dash.security.apache.org lists ASF PMCs only: nothing is read or sent
# for a project that does not declare `organization: ASF`.
try:
org = read_organization(args.project_md)
except OSError as exc:
raise DashError(f"cannot read {args.project_md}: {exc}") from exc
if org != "ASF":
raise NotAsf(
f"{args.project_md} declares organization {org!r}; the ASF Security dashboard "
"covers ASF projects only (organization: ASF)"
)
pmc = check_pmc(args.pmc)
raw = parse_reports(args.input.read_bytes()) if args.input else fetch_reports(pmc, load_cookie())
found = cases.normalise(raw)
Expand Down Expand Up @@ -70,6 +89,13 @@ def _build_parser() -> argparse.ArgumentParser:
command.add_argument(
"--pmc", required=True, help="PMC id, as the dashboard spells it (e.g. airflow)."
)
command.add_argument(
"--project-md",
type=Path,
required=True,
help="The project's resolved <project-config>/project.md; the bridge runs only "
"when it declares organization: ASF.",
)
command.add_argument(
"--input",
type=Path,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class DashError(Exception):
"""Raised when the bridge cannot complete a request."""


class NotAsf(DashError):
"""The project is not an ASF project, so the dashboard has nothing for it."""


class SessionExpired(DashError):
"""The session cookie is missing, rejected, or has expired."""

Expand Down
Loading
Loading