diff --git a/CHANGES.rst b/CHANGES.rst index 232f4f83e22..6001fd22035 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -59,6 +59,9 @@ Features added such as :py:mod:`time` or :py:mod:`datetime` in :file:`conf.py`. See :ref:`the docs ` for further detail. Patch by Adam Turner. +* #11781: Add roles for referencing CVEs (:rst:role:`:cve: `) + and CWEs (:rst:role:`:cwe: `). + Patch by Hugo van Kemenade. Bugs fixed ---------- diff --git a/doc/usage/restructuredtext/roles.rst b/doc/usage/restructuredtext/roles.rst index ea5d99c9137..4774b06195b 100644 --- a/doc/usage/restructuredtext/roles.rst +++ b/doc/usage/restructuredtext/roles.rst @@ -249,6 +249,34 @@ There is also an :rst:role:`index` role to generate index entries. The following roles generate external links: +.. rst:role:: cve + + A reference to a `Common Vulnerabilities and Exposures`_ record. + This generates appropriate index entries. + The text "CVE *number*\ " is generated; + with a link to an online copy of the specified CVE. + You can link to a specific section by using ``:cve:`number#anchor```. + + .. _Common Vulnerabilities and Exposures: https://www.cve.org/ + + For example: :cve:`2020-10735` + + .. versionadded:: 8.1 + +.. rst:role:: cwe + + A reference to a `Common Weakness Enumeration`_. + This generates appropriate index entries. + The text "CWE *number*\ " is generated; in the HTML output, + with a link to an online copy of the specified CWE. + You can link to a specific section by using ``:cwe:`number#anchor```. + + .. _Common Weakness Enumeration: https://cwe.mitre.org/ + + For example: :cwe:`787` + + .. versionadded:: 8.1 + .. rst:role:: pep A reference to a Python Enhancement Proposal. This generates appropriate diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 056769e1ce1..7c1fbd06252 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -53,6 +53,10 @@ 'image_loading': 'link', 'embed_stylesheet': False, 'cloak_email_addresses': True, + 'cve_base_url': 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-', + 'cve_references': None, + 'cwe_base_url': 'https://cwe.mitre.org/data/definitions/', + 'cwe_references': None, 'pep_base_url': 'https://peps.python.org/', 'pep_references': None, 'rfc_base_url': 'https://datatracker.ietf.org/doc/html/', diff --git a/sphinx/roles.py b/sphinx/roles.py index de9163a901f..9d4b407665d 100644 --- a/sphinx/roles.py +++ b/sphinx/roles.py @@ -196,6 +196,94 @@ def process_link( return result +class CVE(ReferenceRole): + def run(self) -> tuple[list[Node], list[system_message]]: + target_id = f'index-{self.env.new_serialno("index")}' + entries = [ + ( + 'single', + _('Common Vulnerabilities and Exposures; CVE %s') % self.target, + target_id, + '', + None, + ) + ] + + index = addnodes.index(entries=entries) + target = nodes.target('', '', ids=[target_id]) + self.inliner.document.note_explicit_target(target) + + try: + refuri = self.build_uri() + reference = nodes.reference( + '', '', internal=False, refuri=refuri, classes=['cve'] + ) + if self.has_explicit_title: + reference += nodes.strong(self.title, self.title) + else: + title = f'CVE {self.title}' + reference += nodes.strong(title, title) + except ValueError: + msg = self.inliner.reporter.error( + __('invalid CVE number %s') % self.target, line=self.lineno + ) + prb = self.inliner.problematic(self.rawtext, self.rawtext, msg) + return [prb], [msg] + + return [index, target, reference], [] + + def build_uri(self) -> str: + base_url = self.inliner.document.settings.cve_base_url + ret = self.target.split('#', 1) + if len(ret) == 2: + return f'{base_url}{ret[0]}#{ret[1]}' + return f'{base_url}{ret[0]}' + + +class CWE(ReferenceRole): + def run(self) -> tuple[list[Node], list[system_message]]: + target_id = f'index-{self.env.new_serialno("index")}' + entries = [ + ( + 'single', + _('Common Weakness Enumeration; CWE %s') % self.target, + target_id, + '', + None, + ) + ] + + index = addnodes.index(entries=entries) + target = nodes.target('', '', ids=[target_id]) + self.inliner.document.note_explicit_target(target) + + try: + refuri = self.build_uri() + reference = nodes.reference( + '', '', internal=False, refuri=refuri, classes=['cwe'] + ) + if self.has_explicit_title: + reference += nodes.strong(self.title, self.title) + else: + title = f'CWE {self.title}' + reference += nodes.strong(title, title) + except ValueError: + msg = self.inliner.reporter.error( + __('invalid CWE number %s') % self.target, line=self.lineno + ) + prb = self.inliner.problematic(self.rawtext, self.rawtext, msg) + return [prb], [msg] + + return [index, target, reference], [] + + def build_uri(self) -> str: + base_url = self.inliner.document.settings.cwe_base_url + ret = self.target.split('#', 1) + if len(ret) == 2: + return f'{base_url}{int(ret[0])}.html#{ret[1]}' + return f'{base_url}{int(ret[0])}.html' + + class PEP(ReferenceRole): def run(self) -> tuple[list[Node], list[system_message]]: target_id = 'index-%s' % self.env.new_serialno('index') @@ -454,12 +542,17 @@ def code_role( 'download': XRefRole(nodeclass=addnodes.download_reference), # links to anything 'any': AnyXRefRole(warn_dangling=True), + # external links + 'cve': CVE(), + 'cwe': CWE(), 'pep': PEP(), 'rfc': RFC(), + # emphasised things 'guilabel': GUILabel(), 'menuselection': MenuSelection(), 'file': EmphasizedLiteral(), 'samp': EmphasizedLiteral(), + # other 'abbr': Abbreviation(), 'manpage': Manpage(), } diff --git a/tests/test_markup/test_markup.py b/tests/test_markup/test_markup.py index 97529dac863..1a045c6529f 100644 --- a/tests/test_markup/test_markup.py +++ b/tests/test_markup/test_markup.py @@ -160,6 +160,74 @@ def get(name): @pytest.mark.parametrize( ('type', 'rst', 'html_expected', 'latex_expected'), [ + ( + # cve role + 'verify', + ':cve:`2020-10735`', + ( + '

' + 'CVE 2020-10735

' + ), + ( + '\\sphinxAtStartPar\n' + '\\index{Common Vulnerabilities and Exposures@\\spxentry{Common Vulnerabilities and Exposures}' + '!CVE 2020\\sphinxhyphen{}10735@\\spxentry{CVE 2020\\sphinxhyphen{}10735}}' + '\\sphinxhref{https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-10735}' + '{\\sphinxstylestrong{CVE 2020\\sphinxhyphen{}10735}}' + ), + ), + ( + # cve role with anchor + 'verify', + ':cve:`2020-10735#id1`', + ( + '

' + 'CVE 2020-10735#id1

' + ), + ( + '\\sphinxAtStartPar\n' + '\\index{Common Vulnerabilities and Exposures@\\spxentry{Common Vulnerabilities and Exposures}' + '!CVE 2020\\sphinxhyphen{}10735\\#id1@\\spxentry{CVE 2020\\sphinxhyphen{}10735\\#id1}}' + '\\sphinxhref{https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-10735\\#id1}' + '{\\sphinxstylestrong{CVE 2020\\sphinxhyphen{}10735\\#id1}}' + ), + ), + ( + # cwe role + 'verify', + ':cwe:`787`', + ( + '

' + 'CWE 787

' + ), + ( + '\\sphinxAtStartPar\n' + '\\index{Common Weakness Enumeration@\\spxentry{Common Weakness Enumeration}' + '!CWE 787@\\spxentry{CWE 787}}' + '\\sphinxhref{https://cwe.mitre.org/data/definitions/787.html}' + '{\\sphinxstylestrong{CWE 787}}' + ), + ), + ( + # cwe role with anchor + 'verify', + ':cwe:`787#id1`', + ( + '

' + 'CWE 787#id1

' + ), + ( + '\\sphinxAtStartPar\n' + '\\index{Common Weakness Enumeration@\\spxentry{Common Weakness Enumeration}' + '!CWE 787\\#id1@\\spxentry{CWE 787\\#id1}}' + '\\sphinxhref{https://cwe.mitre.org/data/definitions/787.html\\#id1}' + '{\\sphinxstylestrong{CWE 787\\#id1}}' + ), + ), ( # pep role 'verify',