Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

scripts: zephyr_module: Add CPE support #66495

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
6 changes: 4 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1675,9 +1675,8 @@ if(CONFIG_BUILD_OUTPUT_BIN AND CONFIG_BUILD_OUTPUT_UF2)
set(BYPRODUCT_KERNEL_UF2_NAME "${PROJECT_BINARY_DIR}/${KERNEL_UF2_NAME}" CACHE FILEPATH "Kernel uf2 file" FORCE)
endif()

set(KERNEL_META_PATH ${PROJECT_BINARY_DIR}/${KERNEL_META_NAME} CACHE INTERNAL "")
if(CONFIG_BUILD_OUTPUT_META)
set(KERNEL_META_PATH ${PROJECT_BINARY_DIR}/${KERNEL_META_NAME} CACHE INTERNAL "")

list(APPEND
post_build_commands
COMMAND ${PYTHON_EXECUTABLE} ${ZEPHYR_BASE}/scripts/zephyr_module.py
Expand All @@ -1691,6 +1690,9 @@ if(CONFIG_BUILD_OUTPUT_META)
post_build_byproducts
${KERNEL_META_PATH}
)
else(CONFIG_BUILD_OUTPUT_META)
# Prevent spdx to use invalid data
file(REMOVE ${KERNEL_META_PATH})
endif()

# Cleanup intermediate files
Expand Down
39 changes: 39 additions & 0 deletions doc/develop/modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,45 @@ Build files located in a ``MODULE_EXT_ROOT`` can be described as:
This allows control of the build inclusion to be described externally to the
Zephyr module.

.. _modules-vulnerability-monitoring:

Vulnerability monitoring
========================

The module description file :file:`zephyr/module.yml` can be used to improve vulnerability monitoring.

If your module needs to track vulnerabilities using an external reference
(e.g your module is forked from another repository), you can use the ``security`` section.
It contains the field ``external-references`` that contains a list of references that needs to
be monitored for your module. The supported formats are:

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good.

Maybe at this location specify the version: field should be used.
For example:

Suggested change
When a module monitors vulnerabilities then the module should also define the `version` field, :ref:`module_version`.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your comment makes me think this version field creates confusion. There was a misunderstanding from my part in the original issue.

The goal was to provide a version for the module and help complete the SBOM for the module (and not searching vulnerabilities from the forked repository for example). The external-references field must be used for that (and CPE and PURL specifications allow to include the version)

I think we should remove the version field from this PR and I will clarify this for a next PR. Are you okay with this ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that there are some clarification (raised by your comment) to be done on this PR with the security group. In the meantime, I converted it into draft and I'll update it when I have all the information I need.

I'm sorry for this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you believe it clarifies the usage and intentions of the field, then it's fine.

The clearer the intention of a PR, the better.

- CPE (Common Platform Enumeration)
- PURL (Package URL)

.. code-block:: yaml
kartben marked this conversation as resolved.
Show resolved Hide resolved

security:
external-references:
kartben marked this conversation as resolved.
Show resolved Hide resolved
- <module-related-cpe>
- <an-other-module-related-cpe>
- <module-related-purl>

A real life example for `mbedTLS` module could look like this:

.. code-block:: yaml

security:
external-references:
- cpe:2.3:a:arm:mbed_tls:3.5.2:*:*:*:*:*:*:*
- pkg:github/Mbed-TLS/[email protected]
Comment on lines +599 to +602
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually poses a good question regarding what CPE and PURLs should be used for the Zephyr forks of the upstream project.
Will it be expected that e.g. the https://github.com/zephyrproject-rtos/mbedtls module shall reference the CPE/PURL of the most recently released version of the upstream mbedtls project?

BTW I think it would be great if the sample code block would show a bit more of the "surroundings" so that folks have an immediate clue regarding the nesting level of the security key without having to look at the schema, i.e

name: mbedtls
build:
  ...
security:
   external-references:
       - cpe:2.3:a:arm:mbed_tls:3.5.2:*:*:*:*:*:*:*
       - pkg:github/Mbed-TLS/[email protected]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Zephyr forks must used the CPE/PURL with the version it forked for the current version. For example, for mbedTLS, the last commit merge the upstream 3.5.2 into the zephyr fork (zephyrproject-rtos/mbedtls@6ec4abd) , so module.yml must contain the example I described.

For the example, I did not want to add keys that are not related to my modification (it would probably not be updated if a changes occurs in build for example). Let me know what I should do.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I now understand why you wouldn't list other keys in case the schema changes, still wonder if it would make sense to at least mention something like " ... you can use the top-level security section" or something, just to help people know quicker where they need to put it

Re: CPE/PURL, part of my question was also (but really just a question for my own education and not something that should get in the way of this PR in any way) how to deal with the Zephyr module forks where it's more than just mirroring the upstream module but also including our own patches (which can come with their own vulnerabilities) on top of it. Should the "external references" list both the CPE/PURL of the upstream module + a CPE/PURL corresponding to the Zephyr fork?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are totally right. This was part of the initial discussion (#53479). At first I wanted zephyr modules maintainers to have the responsibility of the CVE for their project. We finally decided, for now, it wouldn't be the case.
For now, CVE in zephyr modules are not reported (no CPE or via Github Advisiory Database). This is something I would like to change.
This PR is part of multiple PR (I hope) that could lead to what you propose.

The future steps I would like to have:

  • add VEX support in module.yml (paths to VEX)
  • automate vulnerabilities detection from upstream (in zephyr pipeline)
  • add a CPE for each zephyr modules

But this is a discussion to have with the security team / @ceolin and see what's possible or not. Let's take this step by step :)

I hope it answers your question.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope it answers your question.

it very much does :) Thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, ideally Zephyr forks will have their on CPE, but adding it right now would be useless since these CPEs would no match any CVE database. From a practical point of view, what we really need now is a way to monitor CVEs in the projects we have forked.


.. note::
CPE field must follow the CPE 2.3 schema provided by `NVD
<https://csrc.nist.gov/projects/security-content-automation-protocol/specifications/cpe>`_.
PURL field must follow the PURL specification provided by `Github
<https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst>`_.


Build system integration
========================

Expand Down
2 changes: 2 additions & 0 deletions doc/develop/west/zephyr-cmds.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ This generates the following SPDX bill-of-materials (BOM) documents in
- :file:`app.spdx`: BOM for the application source files used for the build
- :file:`zephyr.spdx`: BOM for the specific Zephyr source code files used for the build
- :file:`build.spdx`: BOM for the built output files
- :file:`modules-deps.spdx`: BOM for modules dependencies. Check
:ref:`modules <modules-vulnerability-monitoring>` for more details.

Each file in the bill-of-materials is scanned, so that its hashes (SHA256 and
SHA1) can be recorded, along with any detected licenses if an
Expand Down
12 changes: 12 additions & 0 deletions scripts/west_commands/zspdx/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ def __init__(self):
# primary package purpose (ex. "LIBRARY", "APPLICATION", etc.)
self.primaryPurpose = ""

# package URL
self.url = ""

# package version
self.version = ""

# package revision
self.revision = ""

# package external references
self.externalReferences = []

# the Package's declared license
self.declaredLicense = "NOASSERTION"

Expand Down
6 changes: 6 additions & 0 deletions scripts/west_commands/zspdx/sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,10 @@ def makeSPDX(cfg):
log.err("SPDX writer failed for build document; bailing")
return False

# write modules document
writeSPDX(os.path.join(cfg.spdxDir, "modules-deps.spdx"), w.docModulesExtRefs)
if not retval:
log.err("SPDX writer failed for modules-deps document; bailing")
return False

return True
136 changes: 111 additions & 25 deletions scripts/west_commands/zspdx/walker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import os
import yaml
import re

from west import log
from west.util import west_topdir, WestNotFound
Expand Down Expand Up @@ -47,6 +48,7 @@ def __init__(self, cfg):
self.docZephyr = None
self.docApp = None
self.docSDK = None
self.docModulesExtRefs = None

# dict of absolute file path => the Document that owns that file
self.allFileLinks = {}
Expand All @@ -69,6 +71,36 @@ def __init__(self, cfg):
# SDK install path from parsed CMake cache
self.sdkPath = ""

def _build_purl(self, url, version=None):
if not url:
return None

purl = None
# This is designed to match repository with the following url pattern:
# '<protocol><base_url>/<namespace>/<package>
COMMON_GIT_URL_REGEX=r'((git@|http(s)?:\/\/)(?P<base_url>[\w\.@]+)(\/|:))(?P<namespace>[\w,\-,\_]+)\/(?P<package>[\w,\-,\_]+)(.git){0,1}((\/){0,1})$'

match = re.fullmatch(COMMON_GIT_URL_REGEX, url)
if match:
purl = f'pkg:{match.group("base_url")}/{match.group("namespace")}/{match.group("package")}'

if purl and (version or len(version) > 0):
purl += f'@{version}'

return purl

def _add_describe_relationship(self, doc, cfgpackage):
# create DESCRIBES relationship data
rd = RelationshipData()
rd.ownerType = RelationshipDataElementType.DOCUMENT
rd.ownerDocument = doc
rd.otherType = RelationshipDataElementType.PACKAGEID
rd.otherPackageID = cfgpackage.spdxID
rd.rlnType = "DESCRIBES"

# add it to pending relationships queue
self.pendingRelationships.append(rd)

# primary entry point
def makeDocuments(self):
# parse CMake cache file and get compiler path
Expand Down Expand Up @@ -163,16 +195,7 @@ def setupAppDocument(self):
pkgApp = Package(cfgPackageApp, self.docApp)
self.docApp.pkgs[pkgApp.cfg.spdxID] = pkgApp

# create DESCRIBES relationship data
rd = RelationshipData()
rd.ownerType = RelationshipDataElementType.DOCUMENT
rd.ownerDocument = self.docApp
rd.otherType = RelationshipDataElementType.PACKAGEID
rd.otherPackageID = cfgPackageApp.spdxID
rd.rlnType = "DESCRIBES"

# add it to pending relationships queue
self.pendingRelationships.append(rd)
self._add_describe_relationship(self.docApp, cfgPackageApp)

def setupBuildDocument(self):
# set up build document
Expand All @@ -196,7 +219,7 @@ def setupBuildDocument(self):
# add it to pending relationships queue
self.pendingRelationships.append(rd)

def setupZephyrDocument(self, modules):
def setupZephyrDocument(self, zephyr, modules):
# set up zephyr document
cfgZephyr = DocumentConfig()
cfgZephyr.name = "zephyr-sources"
Expand All @@ -217,40 +240,66 @@ def setupZephyrDocument(self, modules):
cfgPackageZephyr.spdxID = "SPDXRef-zephyr-sources"
cfgPackageZephyr.relativeBaseDir = relativeBaseDir

zephyr_url = zephyr.get("remote", "")
if zephyr_url:
cfgPackageZephyr.url = zephyr_url
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand https://spdx.github.io/spdx-spec/v2.3/package-information/#771-description correctly, I think the URL should be more than "just" the https:// url to the github repo ; i.e. it should be in the form
git+https://github.com/foo/bar.git@revision (with revision probably being the same as PackageVersion), no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the generateDowloadUrl function to follow SPDX specs. Only git is supported.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if looking at the examples given in 7.7.3, then it states:

For Git:

SPDX supported schemes are: git, git+git, git+https, git+http, and git+ssh. git and git+git are equivalent.

Here are the supported forms:

PackageDownloadLocation: git://git.myproject.org/MyProject

PackageDownloadLocation: git+https://git.myproject.org/MyProject.git

PackageDownloadLocation: git+http://git.myproject.org/MyProject
...

so http:// or https:// without git, doesn't seem to be sufficient.


if zephyr.get("revision"):
cfgPackageZephyr.revision = zephyr.get("revision")

purl = None
zephyr_tags = zephyr.get("tags", "")
if zephyr_tags:
# Find tag vX.Y.Z
for tag in zephyr_tags:
version = re.fullmatch(r'^v(?P<version>\d+\.\d+\.\d+)$', tag)
purl = self._build_purl(zephyr_url, tag)

if purl:
cfgPackageZephyr.externalReferences.append(purl)

# Extract version from tag once
if cfgPackageZephyr.version == "" and version:
cfgPackageZephyr.version = version.group('version')

if len(cfgPackageZephyr.version) > 0:
cpe = f'cpe:2.3:o:zephyrproject:zephyr:{cfgPackageZephyr.version}:-:*:*:*:*:*:*'
cfgPackageZephyr.externalReferences.append(cpe)

pkgZephyr = Package(cfgPackageZephyr, self.docZephyr)
self.docZephyr.pkgs[pkgZephyr.cfg.spdxID] = pkgZephyr

self._add_describe_relationship(self.docZephyr, cfgPackageZephyr)

for module in modules:
module_name = module.get("name", None)
module_path = module.get("path", None)
module_url = module.get("remote", None)
module_revision = module.get("revision", None)

if not module_name:
log.err(f"cannot find module name in meta file; bailing")
return False

# Replace "_" by "-" since it's not allowed in spdx ID
module_name = module_name.replace("_", "-")

# set up zephyr sources package
cfgPackageZephyrModule = PackageConfig()
cfgPackageZephyrModule.name = module_name
cfgPackageZephyrModule.name = module_name + "-sources"
cfgPackageZephyrModule.spdxID = "SPDXRef-" + module_name + "-sources"
cfgPackageZephyrModule.relativeBaseDir = module_path
cfgPackageZephyrModule.primaryPurpose = "SOURCE"

if module_revision:
cfgPackageZephyrModule.revision = module_revision

if module_url:
cfgPackageZephyrModule.url = module_url

pkgZephyrModule = Package(cfgPackageZephyrModule, self.docZephyr)
self.docZephyr.pkgs[pkgZephyrModule.cfg.spdxID] = pkgZephyrModule

# create DESCRIBES relationship data
rd = RelationshipData()
rd.ownerType = RelationshipDataElementType.DOCUMENT
rd.ownerDocument = self.docZephyr
rd.otherType = RelationshipDataElementType.PACKAGEID
rd.otherPackageID = cfgPackageZephyr.spdxID
rd.rlnType = "DESCRIBES"
self._add_describe_relationship(self.docZephyr, cfgPackageZephyrModule)

# add it to pending relationships queue
self.pendingRelationships.append(rd)
return True

def setupSDKDocument(self):
# set up SDK document
Expand Down Expand Up @@ -280,6 +329,40 @@ def setupSDKDocument(self):
# add it to pending relationships queue
self.pendingRelationships.append(rd)

def setupModulesDocument(self, modules):
# set up zephyr document
cfgModuleExtRef = DocumentConfig()
cfgModuleExtRef.name = "modules-deps"
cfgModuleExtRef.namespace = self.cfg.namespacePrefix + "/modules-deps"
cfgModuleExtRef.docRefID = "DocumentRef-modules-deps"
self.docModulesExtRefs = Document(cfgModuleExtRef)

for module in modules:
Copy link
Collaborator

@kartben kartben Mar 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not an SPDX expert so please pardon the dumb question but does it really make sense to list all modules enabled in the workspace, even the ones (and that's potentially a long list) that don't end up being part of the final application?

Related: AFAICS there doesn't seem to be a way to figure out what modules are actually used, or what files/packages (as per the contents of build.spdx and zephyr.spdx) "belong" to what module? So how would one know if they are effectively impacted by a vulnerability reported against a particular package?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a question in the first PR I did (#66182 (comment)). I'm not sure what we should do on this, so for now, I did not changed this.
If there is an agreement that we should remove them, I'm okay with this. Maybe in an other PR since it's not the goal of this PR ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SPDX does not require listing the entire set of modules. SPDX is essentially a language that lets you describe a set of software packages to a greater or lesser degree of specificity.

@kartben I'm happy to share some thoughts about how to potentially use these sorts of SPDX documents to "trace" the impact of a vulnerability, but agree with @tgagneret-embedded that it probably makes more sense in a separate issue or PR thread.

module_name = module.get("name", None)
module_security = module.get("security", None)

if not module_name:
log.err(f"cannot find module name in meta file; bailing")
return False

module_ext_ref = []
if module_security:
module_ext_ref = module_security.get("external-references")

# set up zephyr sources package
cfgPackageModuleExtRef = PackageConfig()
cfgPackageModuleExtRef.name = module_name + "-deps"
cfgPackageModuleExtRef.spdxID = "SPDXRef-" + module_name + "-deps"

for ref in module_ext_ref:
cfgPackageModuleExtRef.externalReferences.append(ref)

pkgModule = Package(cfgPackageModuleExtRef, self.docModulesExtRefs)
self.docModulesExtRefs.pkgs[pkgModule.cfg.spdxID] = pkgModule

self._add_describe_relationship(self.docModulesExtRefs, cfgPackageModuleExtRef)


# set up Documents before beginning
def setupDocuments(self):
log.dbg("setting up placeholder documents")
Expand All @@ -289,7 +372,8 @@ def setupDocuments(self):
try:
with open(self.metaFile) as file:
content = yaml.load(file.read(), yaml.SafeLoader)
self.setupZephyrDocument(content["modules"])
if not self.setupZephyrDocument(content["zephyr"], content["modules"]):
return False
except (FileNotFoundError, yaml.YAMLError):
log.err(f"cannot find a valid zephyr_meta.yml required for SPDX generation; bailing")
return False
Expand All @@ -299,6 +383,8 @@ def setupDocuments(self):
if self.cfg.includeSDK:
self.setupSDKDocument()

self.setupModulesDocument(content["modules"])

return True

# walk through targets and gather information
Expand Down
Loading
Loading