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 @@ -107,13 +107,14 @@ When nothing is found the generator exits with an error listing these options.
### Keys

Keys marked *required* have no default: a missing one stops the generator.
The `--repo`, `--vendor`, `--product`, `--package-name`, `--collection-url` and `--org-id` flags override the matching value for a single run.
The `--repo`, `--vendor`, `--product`, `--package-name`, `--collection-url`, `--org-id` and `--cve-tool-url` flags override the matching value for a single run.

| Key | Type | Required | Default | Meaning |
|---|---|---|---|---|
| `[meta].tracker_repo` | string | required | — | The tracker's `owner/name` slug: the default `--repo`, and its owner segment filters tracker URLs out of `references[]`. |
| `[meta].generator_tag` | string | required | — | Written to the record's `x_generator.engine`, so a reader can tell which tool produced it. |
| `[meta].skill_source_url` | string | optional | `https://github.com/<tracker_repo>` | Link to the generator printed in the CVE JSON comment it attaches to the tracker. |
| `[meta].cve_tool_url` | string | optional | `https://cveprocess.apache.org` | Base URL of the CVE tool the project publishes through. Used for the CVE link in the attached comment, the paste hint, and the host filtered out of `references[]`; a non-ASF adopter sets its own tool's URL. A bare host is accepted, with `https://` assumed. |
| `[product].vendor` | string | required | — | `affected[].vendor`. |
| `[product].default_product` | string | required | — | `affected[].product` for an *Affected versions* line that names no known package. |
| `[product].default_package_name` | string | required | — | The package name for such a line. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ def _populate_constants() -> None:
global DEFAULT_REPO, DEFAULT_VENDOR, DEFAULT_PRODUCT
global DEFAULT_PACKAGE_NAME, DEFAULT_COLLECTION_URL, DEFAULT_PURL_TYPE, DEFAULT_PURL_NAMESPACE
global DEFAULT_ASF_ORG_ID, GENERATOR_TAG, SKILL_SOURCE_URL
global DEFAULT_CVE_TOOL_URL
global PROJECT_DISPLAY_MAP, PACKAGE_RE
global TOP_LEVEL_NAME, TOP_LEVEL_PRODUCT, PROJECT_PRODUCT_TEMPLATE
global CNA_PRIVATE_PROJECT_URL, CNA_PRIVATE_OWNER, CNA_PRIVATE_USERS_LIST
Expand All @@ -254,6 +255,7 @@ def _populate_constants() -> None:
"(ASF projects: cve_authority.vulnogram.asf_org_id in organizations/ASF/organization.md)"
)
DEFAULT_ASF_ORG_ID = org_id
DEFAULT_CVE_TOOL_URL = _cve_tool_base(cfg["meta"].get("cve_tool_url", "https://cveprocess.apache.org"))
GENERATOR_TAG = cfg["meta"]["generator_tag"]
SKILL_SOURCE_URL = cfg["meta"].get(
"skill_source_url", f"https://github.com/{cfg['meta']['tracker_repo']}"
Expand Down Expand Up @@ -348,6 +350,9 @@ def _populate_constants() -> None:
# Composer vendor, a Go module prefix. Ignored for types that have none.
DEFAULT_PURL_NAMESPACE: str = ""
DEFAULT_ASF_ORG_ID: str = ""
# Base URL of the CVE tool this project allocates and publishes through.
# ASF default; a non-ASF adopter sets [meta].cve_tool_url in its config.
DEFAULT_CVE_TOOL_URL: str = "https://cveprocess.apache.org"
GENERATOR_TAG: str = ""
SKILL_SOURCE_URL: str = ""
PROJECT_DISPLAY_MAP: dict[str, str] = {}
Expand All @@ -365,6 +370,24 @@ def _populate_constants() -> None:
FORWARD_STATE_LABELS: set[str] = set()
INTERNAL_MARKERS: list[str] = []


def _cve_tool_base(cve_tool_url: object = None) -> str:
"""The CVE tool's base URL: `https://` assumed when the scheme is omitted, no trailing slash."""
raw = str(cve_tool_url or DEFAULT_CVE_TOOL_URL).strip()
return (raw if "://" in raw else f"https://{raw}").rstrip("/")


def _cve_tool_host(cve_tool_url: object = None) -> str | None:
"""The CVE tool's host, for filtering its URLs out of `references[]`.

`hostname` rather than `netloc`: a configured port must not stop a plain
URL on the same host from matching. None for the public CVE record
(`cve.org`, the `independent` default), whose URLs are public references.
"""
host = (urllib.parse.urlparse(_cve_tool_base(cve_tool_url)).hostname or "").lower()
return None if host in {"cve.org", "www.cve.org"} else host


# CVE 5.x convention values that are not project-specific.
DEFAULT_CREDIT_TYPE = "finder"
TOOL_CREDIT_TYPE = "tool"
Expand Down Expand Up @@ -1091,6 +1114,7 @@ def build_references(
mailing_list_field: str,
pr_field: str,
extra_urls: list[str] | None = None,
cve_tool_url: str | None = None,
) -> list[dict]:
"""Collect every URL from the relevant fields and emit a sorted,
de-duplicated CVE ``references`` list with automatic tags.
Expand All @@ -1115,8 +1139,12 @@ def build_references(
gathered: list[str] = list(parse_url_list(pr_field))
if extra_urls:
gathered.extend(extra_urls)
tool_host = _cve_tool_host(cve_tool_url)
filtered = [
url for url in gathered if "cveprocess.apache.org" not in url and TRACKER_FILTER_TOKEN not in url
url
for url in gathered
if (urllib.parse.urlparse(url).hostname or "").lower() != tool_host
and TRACKER_FILTER_TOKEN not in url
]
# De-duplicate preserving order, then sort alphabetically.
seen: set[str] = set()
Expand Down Expand Up @@ -1542,6 +1570,7 @@ def build_cna_container(
purl_namespace: str | None = None,
product_overrides: dict[str, str] | None = None,
current_cve_id: str | None = None,
cve_tool_url: str | None = None,
) -> dict:
# Sibling-CVE cross-references — extract every distinct CVE-YYYY-NNNNN
# mentioned in the description (the short public summary) and emit a
Expand Down Expand Up @@ -1570,7 +1599,9 @@ def build_cna_container(
"metrics": build_metrics(severity_value),
"problemTypes": build_problem_types(cwe_value),
"providerMetadata": {"orgId": org_id},
"references": build_references(mailing_list_value, pr_value, extra_urls=extra_urls),
"references": build_references(
mailing_list_value, pr_value, extra_urls=extra_urls, cve_tool_url=cve_tool_url
),
"source": {"discovery": discovery},
"title": title,
"x_generator": {"engine": GENERATOR_TAG},
Expand Down Expand Up @@ -2127,6 +2158,7 @@ def _build_attachment_body(
json_text: str,
cna: dict,
cna_private_state: str | None,
cve_tool_url: str | None = None,
) -> str:
"""Return the markdown fragment embedded into the issue body.

Expand All @@ -2150,7 +2182,7 @@ def _build_attachment_body(
byte_count = len(json_text.encode("utf-8"))
cve_heading = "CVE JSON — paste-ready"
if cve_id:
cve_link = f"[`{cve_id}`](https://cveprocess.apache.org/cve5/{cve_id})"
cve_link = f"[`{cve_id}`]({_cve_tool_base(cve_tool_url)}/cve5/{cve_id})"
cve_heading = f"CVE JSON — paste-ready for {cve_link}"
script_link = f"[`generate_cve_json.py`]({SKILL_SOURCE_URL})"
title = (cna.get("title") or "").strip()
Expand Down Expand Up @@ -2413,6 +2445,7 @@ def attach_to_issue(
json_text: str,
cna: dict,
cna_private_state: str | None,
cve_tool_url: str | None = None,
) -> tuple[str, bool]:
"""Embed the CVE-JSON attachment in the issue body; update idempotently.

Expand All @@ -2434,6 +2467,7 @@ def attach_to_issue(
json_text=json_text,
cna=cna,
cna_private_state=cna_private_state,
cve_tool_url=cve_tool_url,
)
issue = _fetch_issue(repo, issue_number)
current_body = issue.get("body") or ""
Expand Down Expand Up @@ -2573,6 +2607,11 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
default=None,
help="CNA assigner org id (default from config: cna.org_id).",
)
parser.add_argument(
"--cve-tool-url",
default=None,
help="Base URL of the CVE tool (default from config: meta.cve_tool_url).",
)
parser.add_argument(
"--version-start",
help=(
Expand Down Expand Up @@ -2760,6 +2799,7 @@ def main(argv: list[str] | None = None) -> int:
args.collection_url = DEFAULT_COLLECTION_URL
if args.org_id is None:
args.org_id = DEFAULT_ASF_ORG_ID
cve_tool_url = _cve_tool_base(args.cve_tool_url or DEFAULT_CVE_TOOL_URL)

if args.attach and args.stdin:
print(
Expand Down Expand Up @@ -2963,6 +3003,7 @@ def main(argv: list[str] | None = None) -> int:
advisory_urls=combined_advisory_urls,
product_overrides=product_overrides,
current_cve_id=cve_id,
cve_tool_url=cve_tool_url,
)
except ValueError as exc:
# parse_affected_versions (and any future fail-loud parser
Expand Down Expand Up @@ -2996,7 +3037,7 @@ def main(argv: list[str] | None = None) -> int:
if cve_id:
print(
"Open the Vulnogram #source tab and paste the file:\n"
f" https://cveprocess.apache.org/cve5/{cve_id}#source"
f" {cve_tool_url.rstrip('/')}/cve5/{cve_id}#source"
)

if args.attach:
Expand All @@ -3017,6 +3058,7 @@ def main(argv: list[str] | None = None) -> int:
unresolved=unresolved,
)
),
cve_tool_url=cve_tool_url,
)
except AttachRefused as exc:
print(f"error: {exc}", file=sys.stderr)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Every command prints one JSON object; rendered text is in its `text` field.
| `refs` | `bare_refs` / `linkify` for `#NNN` and `owner/repo#NNN` outside code spans (any backtick run), fences, links, autolinks and URLs, ported from `pr-management`'s `_BARE_REF`; `tracker_link` for a GitHub, Jira or local-files reference. |
| `titles` | `strip_mail_prefixes`, `strip_pr_title`, `md_title`. |
| `recipients` | `security_cc`: the security CC of every draft, each key from the first layer that declares it. |
| `drafting` | `choose`: Gmail or Maildir (the person over the project), then `backend`: `maildir` when configured, else `oauth_curl`, and `claude_ai_mcp` only without OAuth credentials and with no link in the body; `anchor`: what each backend attaches the draft by; `scan`: tracker references, the CVE-tool URL, bare references, re-opening phrases, CVSS and other ASF projects in a composed reporter draft. |
| `drafting` | `choose`: Gmail or Maildir (the person over the project), then `backend`: `maildir` when configured, else `oauth_curl`, and `claude_ai_mcp` only without OAuth credentials and with no link in the body; `anchor`: what each backend attaches the draft by; `scan`: tracker references, the CVE-tool URL, bare references, re-opening phrases, CVSS and other ASF projects (when `organization` is `ASF`) in a composed reporter draft. The CVE-tool URL comes from `cve_tool_url`; a bare host is accepted, with `https://` assumed. |
| `grammar` | `parse_reply`: the confirmation grammar; anything unplaceable goes to `ambiguous_tokens` for the agent to ask back, conflicting dispositions hold their candidate out of every list, unscoped free text holds everything, and a negation confirms nothing. |
| `keyword` | The `tr -cd` allowlist for search keywords, read from a file, never starting with `-`. |
| `fields` | `read`: template fields of an existing tracker body through github-body-field (`normalise`, then `extract_field`); a field it will not decide on is `undecided`, never guessed or read as empty. |
Expand Down Expand Up @@ -196,7 +196,7 @@ Every command takes `--input F`, a JSON file.
| `labels` | `labels_to_add` (`invalid`), `labels_to_remove` (needs triage, scope, `pr created` / `pr merged` when set), `security_issue_stays`, the `gh issue edit` `command`. |
| `board` | The introspection query `file` and `command`; given `items`, the `archiveProjectV2Item` mutation or the *not on the board* note. |
| `draft-plan` | `action` (`skip`, `ask`, `surface_existing`, `blocked`, `create`), `notification_case`, and for a draft `to`, `cc`, `subject` (`Re: <root subject>`), `backend`, `anchor`; links without OAuth credentials exit `2`. |
| `check-draft` | `clean` and `hits`: tracker reference, CVE-tool URL, bare reference, re-opening phrase, CVSS, other ASF projects, a duplicate's missing canonical CVE. |
| `check-draft` | `clean` and `hits`: tracker reference, CVE-tool URL, bare reference, re-opening phrase, CVSS, other ASF projects when `organization` is `ASF`, a duplicate's missing canonical CVE. |
| `render-close` | The brief closing comment `text`. |
| `render-rollup` | `label`, `label_shell`, `text` (at most 5 quotes, the canned link, exactly one Reporter-notification line), `quotes_dropped`, `needs_amend`, `bare_refs`; no notification case exits `2`. |
| `parse-reply` | `action` (`apply`, `apply_silent`, `replace_email_body`, `repick_canned`, `cancel`, `ambiguous`), `email_body_override`, `canned_section_override`. |
Expand All @@ -217,7 +217,7 @@ Every command takes `--input F`, a JSON file.
| `allocate-result` | `next` (`step-4`, `stop`, `setup`, `check-records`, `recipe`), `cve_id`, `message`, `rerun_allocate` (true only after an expired token). |
| `cve` | `cve_id` and `record_url` from the user's paste-back; none, a malformed token or two IDs exit `2`. |
| `draft-plan` | `action` (`skip`, `ask`, `surface_existing`, `blocked`, `create`), `notification` (`omitted`, `team_member`, `notified`, `draft_pending`) and for a draft `to`, `cc`, `subject`, `backend`, `anchor`, `secondary_threads`. |
| `check-draft` | `clean` and `hits`: a missing CVE ID, the CVE-tool URL, a tracker reference, a bare reference, CVSS, other ASF projects. |
| `check-draft` | `clean` and `hits`: a missing CVE ID, the CVE-tool URL, a tracker reference, a bare reference, CVSS, other ASF projects when `organization` is `ASF`. |
| `proposal` | The numbered `items` (5 only when a draft is created), `proposal_items_count`, the `cve_tool_link_value`, `notes`, `text`. |
| `render-rollup` | `label` (`CVE allocated (<CVE-ID>)`), `label_shell`, `text`, `needs_amend`, `bare_refs`; a draft id not checked against Drafts exits `2`. |
| `parse-reply` | `action` (`apply`, `cancel`, `edit`, `ambiguous`), `items_applied`, `items_skipped`. |
Expand Down Expand Up @@ -253,7 +253,7 @@ Every command takes `--input F`, a JSON file.
| `bucket` | `cve_affecting`, `non_cve_affecting`, `walk_order`, `review_ready_push` and its bucket. |
| `parse-bulk-reply` | `action` (`apply`, `force_sync`, `cancel`, `none`, `ambiguous`), `apply` per tracker, `edits`, `force_sync`, `skip`. |
| `parse-reply` | The single-tracker form through `shared.numbered`. |
| `check` | `clean` and `violations` (`surface`, `guardrail`, `evidence`) for reporter CVSS, other ASF projects, bare CVE IDs or the CVE-tool URL in an email, and bare `#NNN`. |
| `check` | `clean` and `violations` (`surface`, `guardrail`, `evidence`) for reporter CVSS, other ASF projects when `organization` is `ASF`, bare CVE IDs or the CVE-tool URL in an email, and bare `#NNN`. |
| `security-cc` | `security_cc`, `cc_fallback`, `warning` and `warning_text`, `draft_blocked`, `cc`, `search_list`, through the project / organization / framework layers. |
| `draft-backend` | `backend` (`maildir`, `oauth_curl`, `claude_ai_mcp`), `decided_by` (`user.md`, `project.md`, `default`); no credential with links in the body exits `2`. |
| `ghsa-route` | `access_tier`, `action`, `channel`, and for an admin hand-off the draft tool, addressee, thread, rollup marker and three parts. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,13 @@ def _plan_thread(data: Mapping[str, Any], thread: Mapping[str, Any]) -> dict[str

def check(data: Mapping[str, Any]) -> dict[str, Any]:
text = str(data.get("body") or "")
hits = drafting.scan(text, str(data["tracker"]), data.get("own_names") or [])
hits = drafting.scan(
text,
str(data["tracker"]),
data.get("own_names") or [],
data.get("organization"),
data.get("cve_tool_url"),
)
record = one_line(data.get("record_url"))
if record and record in text and not any(h["kind"] == "cve-tool-url" for h in hits):
hits.append({"kind": "cve-tool-url", "line": None, "text": record})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,13 @@ def plan(data: Mapping[str, Any]) -> dict[str, Any]:
def check(data: Mapping[str, Any]) -> dict[str, Any]:
text = str(data.get("body") or "")
tracker = str(data["tracker"])
hits = drafting.scan(text, tracker, data.get("own_names") or [])
hits = drafting.scan(
text,
tracker,
data.get("own_names") or [],
data.get("organization"),
data.get("cve_tool_url"),
)
if str(data.get("disposition") or "invalid") == "duplicate":
canonical = one_line(data.get("canonical_cve"))
if not canonical or canonical not in _CVE.findall(text):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,16 @@
r"also reported against|cross-project|other apache projects|sister project|the same finder also",
re.IGNORECASE,
)


# The CVE-tool host as a URL host, not a substring check: this scans text, it sanitises nothing.
_CVE_TOOL_URL = re.compile(r"(?:https?://)?(?<![\w.-])cveprocess\.apache\.org(?![\w-])", re.IGNORECASE)
def _cve_tool_pattern(cve_tool_url: object = None) -> re.Pattern[str] | None:
host = drafting.cve_tool_host(cve_tool_url)
if host in drafting.PUBLIC_CVE_HOSTS:
return None
return re.compile(r"(?:https?://)?(?<![\w.-])" + re.escape(host) + r"(?![\w-])", re.IGNORECASE)


_BREAK = re.compile(r"(?<=[.!?])\s+")


Expand All @@ -81,8 +89,10 @@ def _bare_cves(sentence: str) -> list[str]:

def check(data: Mapping[str, Any]) -> dict[str, Any]:
own = {str(n).lower() for n in data.get("own_names") or []}
names = [n for n in drafting.ASF_PROJECTS if n.lower() not in own]
other = re.compile(r"\b(?:Apache\s+)?(?:" + "|".join(names) + r")\b")
is_asf = str(data.get("organization") or "ASF").strip().upper() == "ASF" # unset fails closed
names = [n for n in drafting.ASF_PROJECTS if n.lower() not in own] if is_asf else []
other = re.compile(r"\b(?:Apache\s+)?(?:" + "|".join(names) + r")\b") if names else None
cve_tool = _cve_tool_pattern(data.get("cve_tool_url"))
violations: list[dict[str, str]] = []
for surface in data.get("surfaces") or []:
kind = str(surface.get("kind") or "")
Expand All @@ -92,10 +102,10 @@ def check(data: Mapping[str, Any]) -> dict[str, Any]:
found: list[str] = []
if _CVSS.search(sentence):
found.append(CVSS)
if other.search(sentence) or _CROSS.search(sentence):
if (other is not None and other.search(sentence)) or _CROSS.search(sentence):
found.append(OTHER_ASF)
if kind == "email":
if _CVE_TOOL_URL.search(sentence):
if cve_tool is not None and cve_tool.search(sentence):
found.append(LINK_CVE)
elif kind in MARKDOWN:
if _bare_cves(sentence):
Expand Down
Loading
Loading