This is an automated email from the ASF dual-hosted git repository.

pkarwasz pushed a commit to branch feat/openvex-file
in repository https://gitbox.apache.org/repos/asf/commons-text.git

commit 2f05e56f6daf54b60e699506be95bb4f7f451d67
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Thu Jul 31 13:45:45 2025 +0200

    feat: Add script to generate OpenVEX file
    
    This change introduces the `generate_openvex.py` script, which converts the 
`VEX.cyclonedx.xml` file into a compliant OpenVEX JSON document.
    
    ### Highlights
    
    * Adds a Python script to automate VEX conversion from CycloneDX format to 
OpenVEX.
    * Generates a fully populated OpenVEX document based on vulnerability 
analysis data in `VEX.cyclonedx.xml`.
    
    ### Additional Fixes
    
    * Corrects a non-unique `serialNumber` (UUID) that was mistakenly 
copy-pasted from `commons-bcel`.
    * Removes unintended indentation from the explanation text, ensuring valid 
Markdown formatting.
---
 src/conf/security/README.md           |  14 +++
 src/conf/security/VEX.cyclonedx.xml   |  21 +++--
 src/conf/security/generate_openvex.py | 170 ++++++++++++++++++++++++++++++++++
 src/conf/security/openvex.json        |  32 +++++++
 4 files changed, 229 insertions(+), 8 deletions(-)

diff --git a/src/conf/security/README.md b/src/conf/security/README.md
index 8629f82a..e8c492b4 100644
--- a/src/conf/security/README.md
+++ b/src/conf/security/README.md
@@ -40,6 +40,10 @@ An experimental 
[VEX](https://cyclonedx.org/capabilities/vex/) document is also
 
 👉 
[`https://raw.githubusercontent.com/apache/commons-text/refs/heads/master/src/conf/security/VEX.cyclonedx.xml`](VEX.cyclonedx.xml)
 
+It is also available in [OpenVEX format](https://github.com/openvex/spec) at:
+
+👉 
[`https://raw.githubusercontent.com/apache/commons-text/refs/heads/master/src/conf/security/openvex.json`](openvex.json)
+
 This document provides information about the **exploitability of known 
vulnerabilities** in the **dependencies** of Apache Commons Text.
 
 ### When is a dependency vulnerability exploitable?
@@ -59,3 +63,13 @@ Because Apache Commons libraries (including Text) do **not** 
bundle their depend
 * The `analysis` field in the VEX file uses **Markdown** formatting.
 
 For more information about CycloneDX, SBOMs, or VEX, visit 
[cyclonedx.org](https://cyclonedx.org/).
+
+## Contributing
+
+To add or update a VEX entry:
+
+* Edit the CycloneDX VEX document:
+  1. Increase the `version` attribute in the `<bom>` element.
+  2. Update the `timestamp` in the `<metadata>` section.
+  3. Make your changes to the vulnerability information.
+* Regenerate the `openvex.json` file by running the `generate-openvex.sh` 
script.
\ No newline at end of file
diff --git a/src/conf/security/VEX.cyclonedx.xml 
b/src/conf/security/VEX.cyclonedx.xml
index 7ef177f8..85de5662 100644
--- a/src/conf/security/VEX.cyclonedx.xml
+++ b/src/conf/security/VEX.cyclonedx.xml
@@ -19,12 +19,13 @@
   To update this document:
     1. Increment the `version` attribute in the <bom> element.
     2. Update the `timestamp` in the <metadata> section.
+    3. Regenerate the `openvex.json` file using the `generate-openvex.sh` 
script.
 -->
 <bom xmlns="http://cyclonedx.org/schema/bom/1.6";
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
      xsi:schemaLocation="http://cyclonedx.org/schema/bom/1.6 
https://cyclonedx.org/schema/bom-1.6.xsd";
-     serialNumber="urn:uuid:f70dec29-fc7d-41f2-8c60-97e9075e0e73"
-     version="1">
+     serialNumber="urn:uuid:9d64577b-0376-4ee7-b154-5ec26a1803f4"
+     version="2">
 
   <metadata>
     <timestamp>2025-07-29T12:26:42Z</timestamp>
@@ -51,6 +52,10 @@
   <vulnerabilities>
     <vulnerability>
       <id>CVE-2025-48924</id>
+      <source>
+        <name>NVD</name>
+        <url>https://nvd.nist.gov/vuln/detail/CVE-2025-48924</url>
+      </source>
       <references>
         <reference>
           <id>GHSA-j288-q9x7-2f5v</id>
@@ -65,14 +70,14 @@
           <response>update</response>
         </responses>
         <detail>
-          CVE-2025-48924 is exploitable in Apache Commons Text versions 1.5 
and later, but only when all the following conditions are met:
+CVE-2025-48924 is exploitable in Apache Commons Text versions 1.5 and later, 
but only when all the following conditions are met:
 
-          * The consuming project includes a vulnerable version of Commons 
Text on the classpath.
-            As of version `1.14.1`, Commons Text no longer references a 
vulnerable version of the `commons-lang3` library in its POM file.
-          * Unvalidated or unsanitized user input is passed to the 
`StringSubstitutor` or `StringLookup` classes.
-          * An interpolator lookup created via 
`StringLookupFactory.interpolatorLookup()` is used.
+* The consuming project includes a vulnerable version of Commons Text on the 
classpath.
+  As of version `1.14.1`, Commons Text no longer references a vulnerable 
version of the `commons-lang3` library in its POM file.
+* Unvalidated or unsanitized user input is passed to the `StringSubstitutor` 
or `StringLookup` classes.
+* An interpolator lookup created via 
`StringLookupFactory.interpolatorLookup()` is used.
 
-          If these conditions are satisfied, an attacker may cause an infinite 
loop by submitting a specially crafted input such as `${const:...}`.
+If these conditions are satisfied, an attacker may cause an infinite loop by 
submitting a specially crafted input such as `${const:...}`.
         </detail>
         <firstIssued>2025-07-29T12:26:42Z</firstIssued>
         <lastUpdated>2025-07-29T12:26:42Z</lastUpdated>
diff --git a/src/conf/security/generate_openvex.py 
b/src/conf/security/generate_openvex.py
new file mode 100755
index 00000000..b77e0dc8
--- /dev/null
+++ b/src/conf/security/generate_openvex.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python3
+import xml.etree.ElementTree as ET
+import json
+from datetime import datetime, timezone
+
+NAMESPACES = {
+    'b': 'http://cyclonedx.org/schema/bom/1.6'
+}
+
+
+def _find_element(parent: ET.Element, tag: str) -> ET.Element | None:
+    return parent.find(tag, NAMESPACES)
+
+
+def _find_stripped_text(parent: ET.Element, tag: str) -> str | None:
+    el = _find_element(parent, tag)
+    return el.text.strip() if el is not None else None
+
+
+def _add_optional_date(parent: ET.Element, tag: str, target: dict, key: str) 
-> None:
+    el = _find_element(parent, tag)
+    if el is not None and el.text:
+        try:
+            dt = 
datetime.fromisoformat(el.text.strip()).astimezone(timezone.utc)
+            target[key] = dt.isoformat().replace('+00:00', 'Z')
+        except ValueError as e:
+            raise ValueError(f"Invalid ISO date format in <{tag}>: {el.text}") 
from e
+
+
+def load_cyclonedx(path: str = 'VEX.cyclonedx.xml') -> ET.Element:
+    return ET.parse(path).getroot()
+
+
+def to_openvex(root: ET.Element) -> dict:
+    serial_number = root.get('serialNumber')
+    if not serial_number:
+        raise ValueError("CycloneDX BOM must have a 'serialNumber' attribute")
+
+    version = int(root.get('version', '1'))
+
+    result = {
+        '@context': 'https://openvex.dev/ns/v0.2.0',
+        '@id': f"https://commons.apache.org/security/vex/{serial_number}";,
+        'author': 'Apache Commons Security Team <[email protected]>',
+        'role': 'Security Team',
+        'version': version,
+        'tooling': (
+            "This document was automatically converted from the 
`VEX.cyclonedx.xml` file.\n"
+            "Do not edit this file directly, run `generate_openvex.py` to 
regenerate it."
+        )
+    }
+
+    _add_optional_date(root, 'b:metadata/b:timestamp', result, 'timestamp')
+
+    component = _find_element(root, 'b:metadata/b:component')
+    if component is None:
+        raise ValueError("Missing <component> in <metadata>")
+
+    product = to_openvex_product(component)
+
+    result['statements'] = [
+        to_openvex_statement(vuln, product)
+        for vuln in root.findall('.//b:vulnerability', NAMESPACES)
+    ]
+
+    return result
+
+
+def to_openvex_product(component: ET.Element) -> dict:
+    purl = _find_element(component, 'b:purl')
+    if purl is None or not purl.text:
+        raise ValueError("Component must include a non-empty <purl> element")
+
+    return {
+        '@id': purl.text,
+        'identifiers': {
+            'purl': purl.text
+        }
+    }
+
+
+def to_openvex_vulnerability(vuln: ET.Element) -> dict:
+    cdx_id = _find_stripped_text(vuln, 'b:id')
+    if not cdx_id:
+        raise ValueError("Vulnerability must have an <id>")
+
+    entry = {'name': cdx_id}
+
+    source = _find_element(vuln, 'b:source')
+    if source is not None:
+        entry['@id'] = _find_stripped_text(source, 'b:url')
+
+    entry['aliases'] = [
+        _find_stripped_text(ref, 'b:id')
+        for ref in vuln.findall('b:references/b:reference', NAMESPACES)
+    ]
+
+    return entry
+
+
+def to_openvex_statement(vuln: ET.Element, product: dict) -> dict:
+    analysis = _find_element(vuln, 'b:analysis')
+    if analysis is None:
+        raise ValueError("Missing <analysis> in vulnerability")
+
+    state = _find_stripped_text(analysis, 'b:state')
+    if not state:
+        raise ValueError("Missing <state> in vulnerability analysis")
+
+    statement = {
+        'products': [product],
+        'vulnerability': to_openvex_vulnerability(vuln),
+        'status': to_openvex_status(state)
+    }
+
+    justification = _find_stripped_text(analysis, 'b:justification')
+    if justification:
+        statement['justification'] = to_openvex_justification(justification)
+
+    detail = _find_stripped_text(analysis, 'b:detail')
+    if detail:
+        statement['status_notes'] = detail
+
+    _add_optional_date(analysis, 'b:firstIssued', statement, 'timestamp')
+    _add_optional_date(analysis, 'b:lastUpdated', statement, 'last_updated')
+
+    return statement
+
+
+def to_openvex_status(cdx_status: str) -> str:
+    mapping = {
+        "resolved": "fixed",
+        "exploitable": "affected",
+        "in_triage": "under_investigation",
+        "false_positive": "not_affected",
+        "not_affected": "not_affected"
+    }
+    status = mapping.get(cdx_status.strip().lower())
+    if not status:
+        raise ValueError(f"Unknown CycloneDX status: '{cdx_status}'")
+    return status
+
+
+def to_openvex_justification(cdx_justification: str) -> str:
+    mapping = {
+        "code_not_present": "vulnerable_code_not_present",
+        "code_not_reachable": "vulnerable_code_not_in_execute_path",
+        "requires_configuration": 
"vulnerable_code_cannot_be_controlled_by_adversary",
+        "requires_dependency": "component_not_present",
+        "requires_environment": 
"vulnerable_code_cannot_be_controlled_by_adversary",
+        "protected_by_compiler": "inline_mitigations_already_exist",
+        "protected_at_runtime": "inline_mitigations_already_exist",
+        "protected_by_mitigating_control": "inline_mitigations_already_exist"
+    }
+    result = mapping.get(cdx_justification.strip().lower())
+    if not result:
+        raise ValueError(f"Unknown CycloneDX justification: 
'{cdx_justification}'")
+    return result
+
+
+def main():
+    cyclonedx_root = load_cyclonedx()
+    openvex_doc = to_openvex(cyclonedx_root)
+    with open('openvex.json', 'w') as f:
+        json.dump(openvex_doc, f, indent=2)
+    print("OpenVEX document written to 'openvex.json'")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/conf/security/openvex.json b/src/conf/security/openvex.json
new file mode 100644
index 00000000..a287ca50
--- /dev/null
+++ b/src/conf/security/openvex.json
@@ -0,0 +1,32 @@
+{
+  "@context": "https://openvex.dev/ns/v0.2.0";,
+  "@id": 
"https://commons.apache.org/security/vex/urn:uuid:9d64577b-0376-4ee7-b154-5ec26a1803f4";,
+  "author": "Apache Commons Security Team <[email protected]>",
+  "role": "Security Team",
+  "version": 2,
+  "tooling": "This document was automatically converted from the 
`VEX.cyclonedx.xml` file.\nDo not edit this file directly, run 
`generate_openvex.py` to regenerate it.",
+  "timestamp": "2025-07-29T12:26:42Z",
+  "statements": [
+    {
+      "products": [
+        {
+          "@id": "pkg:maven/org.apache.commons/commons-text?type=jar",
+          "identifiers": {
+            "purl": "pkg:maven/org.apache.commons/commons-text?type=jar"
+          }
+        }
+      ],
+      "vulnerability": {
+        "name": "CVE-2025-48924",
+        "@id": "https://nvd.nist.gov/vuln/detail/CVE-2025-48924";,
+        "aliases": [
+          "GHSA-j288-q9x7-2f5v"
+        ]
+      },
+      "status": "affected",
+      "status_notes": "CVE-2025-48924 is exploitable in Apache Commons Text 
versions 1.5 and later, but only when all the following conditions are 
met:\n\n* The consuming project includes a vulnerable version of Commons Text 
on the classpath.\n  As of version `1.14.1`, Commons Text no longer references 
a vulnerable version of the `commons-lang3` library in its POM file.\n* 
Unvalidated or unsanitized user input is passed to the `StringSubstitutor` or 
`StringLookup` classes.\n* An interpol [...]
+      "timestamp": "2025-07-29T12:26:42Z",
+      "last_updated": "2025-07-29T12:26:42Z"
+    }
+  ]
+}
\ No newline at end of file

Reply via email to