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

Add format 'cyclonedx' as output format for the graph command #14405

Closed
wants to merge 2 commits into from
Closed
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
3 changes: 2 additions & 1 deletion conan/cli/commands/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from conan.cli import make_abs_path
from conan.cli.args import common_graph_args, validate_common_graph_args
from conan.cli.command import conan_command, conan_subcommand
from conan.cli.formatters.graph import format_graph_html, format_graph_json, format_graph_dot
from conan.cli.formatters.graph import format_graph_html, format_graph_json, format_graph_dot, format_graph_cyclonedx
from conan.cli.formatters.graph.graph_info_text import format_graph_info
from conan.cli.printers.graph import print_graph_packages, print_graph_basic
from conan.internal.deploy import do_deploys
Expand Down Expand Up @@ -106,6 +106,7 @@ def graph_build_order_merge(conan_api, parser, subparser, *args):
@conan_subcommand(formatters={"text": format_graph_info,
"html": format_graph_html,
"json": format_graph_json,
"cyclonedx": format_graph_cyclonedx,
"dot": format_graph_dot})
def graph_info(conan_api, parser, subparser, *args):
"""
Expand Down
1 change: 1 addition & 0 deletions conan/cli/formatters/graph/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .graph import format_graph_html
from .graph import format_graph_dot
from .graph import format_graph_json
from .graph import format_graph_cyclonedx
41 changes: 41 additions & 0 deletions conan/cli/formatters/graph/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,44 @@ def format_graph_json(result):
cli_out_write(json_result)
if graph.error:
raise graph.error


def format_graph_cyclonedx(result):
"""
# creates a CycloneDX JSON according to https://cyclonedx.org/docs/1.4/json/
"""
def licenses(conanfilelic):
def entry(id):
return {"license": {
"id": id
}}
if conanfilelic is None:
return []
elif isinstance(conanfilelic, str):
return [entry(conanfilelic)]
else:
return [entry(i) for i in conanfilelic]

result["graph"].serialize() # fills ids
deps = result["graph"].nodes[1:] # first node is app itself
cyclonedx = {
"bomFormat": "CycloneDX",

Choose a reason for hiding this comment

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

would you add the schema to the json, so its more clear how to validate the result?

  "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",

"specVersion": "1.4",
"version": 1,
"dependencies": [n.id for n in deps],

Choose a reason for hiding this comment

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

This code looks wrong, according to CycloneDX spec.
Could you provide an example SBOM generated with your code, so we can check if the JSON results is actually valid according to http://cyclonedx.org/schema/bom-1.4.schema.json

Copy link
Author

Choose a reason for hiding this comment

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

Thanks, that is not correct. I will fix it in the conan extension

"components": [
{
"type": "library",
"bom-ref": n.id,
"purl": n.package_url().to_string(),
"licenses": licenses(n.conanfile.license),
"name": n.name,
"version": n.conanfile.version,
"supplier": {
"url": [n.conanfile.homepage]

Choose a reason for hiding this comment

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

homepage - why not externalReferences of type website?
see https://cyclonedx.org/use-cases/#external-references

Copy link
Author

Choose a reason for hiding this comment

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

What is the advantage of using that over supplier? external references are not part of the BOM: https://cyclonedx.org/docs/1.4/json/#components_items_supplier VS https://cyclonedx.org/docs/1.4/json/#components_items_externalReferences

Copy link

@jkowalleck jkowalleck Aug 3, 2023

Choose a reason for hiding this comment

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

What is the advantage of using that over supplier

It's a matter of correctness.
One is the URL to the homepage of a supplier(or contractor, legal proxy, etc),
the other is the URL to the homepage of the component/project.

From what I've seen, n.conanfile.homepage contains the project's homepage, not the homepage of an author or supplier.
see https://docs.conan.io/2/reference/conanfile/attributes.html?highlight=homepage#homepage

I found that n.conanfile.url could be the externalReference of type vcs
see https://docs.conan.io/2/reference/conanfile/attributes.html?highlight=url#url

Linking a component's metadata and URLs via externalReferences is the intended way.
see the many examples here: https://cyclonedx.org/use-cases/

Copy link
Author

Choose a reason for hiding this comment

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

conanfile.url points to the GitHub repo of conan-center. Not sure that is useful in this case.

I will use the homepage attribute and move it to externalReferences

}
} for n in deps
]
}
json_result = json.dumps(cyclonedx, indent=4)
cli_out_write(json_result)
25 changes: 25 additions & 0 deletions conans/client/graph/graph.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from collections import OrderedDict

from packageurl import PackageURL

from conans.model.package_ref import PkgReference
from conans.model.recipe_ref import RecipeReference

Expand Down Expand Up @@ -229,6 +231,29 @@ def serialize(self):
result["test"] = self.test
return result

def package_url(self):
"""
Creates a PURL following https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

Do you want me to change the comment, or is there an error in how I build the JSON?

Choose a reason for hiding this comment

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

wanted to put a more precise link here, so nobody needs to scroll trough the whole document.

"""
qualifiers = {
"prev": self.prev
}
if self.ref.user:
qualifiers["user"] = self.ref.user
if self.ref.channel:
qualifiers["channel"] = self.ref.channel
if self.ref.revision:
qualifiers["rref"] = self.ref.revision
if self.remote:
qualifiers["repository_url"] = self.remote
else:
qualifiers["repository_url"] = "https://center.conan.io"
return PackageURL(
type="conan",
name=self.name,
version=str(self.ref.version),
qualifiers=qualifiers)

def overrides(self):

def transitive_subgraph():
Expand Down
1 change: 1 addition & 0 deletions conans/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ fasteners>=0.15
distro>=1.4.0, <=1.8.0; sys_platform == 'linux' or sys_platform == 'linux2'
Jinja2>=3.0, <4.0.0
python-dateutil>=2.8.0, <3
packageurl-python>=0.10.0
Copy link
Member

Choose a reason for hiding this comment

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

We are avoiding adding new dependencies like this one, unless it is extremely necessary. Every additional Python dependency has proven to be a liability (the state of dependency management in Python is not great), so we prefer to avoid this.