Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ _SoftwareClearing/

!SBOM/*.json
!tests/fixtures/*
!capycli/data/*.json

SoftwareClearing/*.html
SoftwareClearing/*.json
Expand Down
4 changes: 2 additions & 2 deletions .reuse/dep5
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ Files: tests/fixtures/*
Copyright: 2018-2023 Siemens
License: MIT

Files: capycli/data/granularity_list.csv requirements.txt
Copyright: 2018-2023 Siemens
Files: capycli/data/granularity_list.csv requirements.txt capycli/data/component_checks.json
Copyright: 2018-2026 Siemens
License: MIT

Files: Legacy/code.siemens.com-gitlab-ci.yml.txt Legacy/Pipfile.lock
Expand Down
12 changes: 12 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,25 @@

# CaPyCli - Clearing Automation Python Command Line Tool for SW360

## NEXT

* Two new commands: `bom componentcheck` and `project componentcheck`. The first one
checks a given SBOM for special components, the second one does the same for
an existing SW360 project. **Special components** are components that should not be
part of license compliance checks. These are for example unit test tools like
`junit` or `pytest`, linter like `eslint`, mocking frameworks like `Moq`, etc.
CaPyCLI has a list of these components (data/component_checks.json), but you
can also provide your own list. For more information please have a look at [this documentation](documentation/Readme_Componentcheck.md).
* New folder `documentation` where we want to keep all more detailed documentation
on the way CaPyCLI works.

## 2.10.1

* `bom show` now also shows the group, if it exists.
* Improve dependency detection in `getdependencies javascript`.
* Fix issue in `project prerequisites` when reading an empty project.

## 2.10.0

Check failure on line 26 in ChangeLog.md

View workflow job for this annotation

GitHub Actions / build (3.11)

Multiple headings with the same content [Context: "## NEXT"]

* Have `bom bompackage` as a separate command and have the advanced folder structure
based on SHA1 hashes.
Expand Down
8 changes: 4 additions & 4 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,14 @@ These options are not available for all commands. At the moment

Over the time we implemented more and more commands with more and more parameters.
We understand that it is hard for beginners to find the right command for the task
they want to do. Have a look at our [Use Case Overview](UseCaseOverview.md).
they want to do. Have a look at our [Use Case Overview](documentation/UseCaseOverview.md).

## Software Clearing Approaches

From time to time there are questions **why** a command has been implemented in this
specific way or why a command exists at all. Not all organization have the same
approach when doing license compliance. Have a look at our
[Software Clearing Approach Overview](SoftwareClearingApproachOverview.md) to see our
[Software Clearing Approach Overview](documentation/SoftwareClearingApproachOverview.md) to see our
approaches.

## Note about Python Dependency Detection
Expand Down Expand Up @@ -233,7 +233,7 @@ The software bill of materials (SBOM) is a crucial information for most operatio
There is no common description what a bill of materials should contain.
There are different formats available, for example the SBOM of CyCloneDX,
nevertheless most tools have their own SBOM format.
We have decided also to have our own flavor of CycloneDX, see [SBOM](Readme_BOM.md),
We have decided also to have our own flavor of CycloneDX, see [SBOM](documentation/Readme_BOM.md),
focused on the information we need to handle components, releases and projects
on SW360. It is a simple JSON format. CaPyCli reads or writes exactly the
information that is needed.
Expand All @@ -243,7 +243,7 @@ to refer you to the open source tools from [CycloneDX](https://cyclonedx.org/).

## Mapping a SBOM to SW360

SBOM mapping is described in an extra file, see [SBOM Mapping](Readme_Mapping.md).
SBOM mapping is described in an extra file, see [SBOM Mapping](documentation/Readme_Mapping.md).

## Project Management

Expand Down
212 changes: 212 additions & 0 deletions capycli/bom/component_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# -------------------------------------------------------------------------------
# Copyright (c) 2026 Siemens
# All Rights Reserved.
# Author: thomas.graf@siemens.com
#
# SPDX-License-Identifier: MIT
# -------------------------------------------------------------------------------

try:
import importlib.resources as pkg_resources
except ImportError:
# Try backported to PY<37 `importlib_resources`.
import importlib_resources as pkg_resources # type: ignore

import json
import os
import sys
from typing import Any, Dict, List

import requests

# from cyclonedx.model import ExternalReferenceType
from cyclonedx.model.bom import Bom
from cyclonedx.model.component import Component

import capycli.common.script_base
from capycli.common.capycli_bom_support import CaPyCliBom
from capycli.common.print import print_red, print_text, print_yellow
from capycli.main.result_codes import ResultCode

LOG = capycli.get_logger(__name__)


class ComponentCheck(capycli.common.script_base.ScriptBase):
"""
Check the SBOM for special components.
"""
def __init__(self) -> None:
self.component_check_list: Dict[str, Any] = {}
self.files_to_ignore: List[Dict[str, Any]] = []

@staticmethod
def get_component_check_list(download_url: str) -> None:
"""This will only download the component check file from a public repository."""
response = requests.get(download_url)
response.raise_for_status()
with open("component_checks.json", "wb") as f1:
f1.write(response.content)

def read_component_check_list(self, download_url: str = "", local_check_list_file: str = "") -> None:
"""Reads the granularity list from file."""
self.component_check_list = {}
text_list = ""
if local_check_list_file:
try:
with open(local_check_list_file, "r", encoding="utf-8") as fin:
self.component_check_list = json.load(fin)
except FileNotFoundError as e:
print_yellow(f"File not found: {e} \n Reading the default component check list")
except Exception as e:
print_red(f"An unexpected error occurred: {e}")
if download_url:
try:
ComponentCheck.get_component_check_list(download_url)
with open("component_checks.json", "r", encoding="utf-8") as fin:
self.component_check_list = json.load(fin)
except FileNotFoundError as e:
print_yellow(f"File not found: {e} \n Reading the default component check list")
except Exception as e:
print_red(f"An unexpected error occurred: {e}")
if not self.component_check_list:
if sys.version_info >= (3, 9):
resources = pkg_resources.files("capycli.data")
text_list = (resources / "component_checks.json").read_text()
else:
text_list = pkg_resources.read_text("capycli.data", "component_checks.json")

self.component_check_list = json.loads(text_list)

def get_dev_dependencies(self, ecosystem: str) -> List[Dict[str, Any]]:
"""Get the list of dependencies for a specific eco-system."""
dd = self.component_check_list.get("dev_dependencies", [])
if not dd:
return []

return dd.get(ecosystem, [])

def is_dev_dependency(self, comp: Component) -> bool:
"""Check whether the given component matches any known development dependency."""
if comp.purl:
# preferred: check by package-url
pd = comp.purl.to_dict()
ecosystem = pd.get("type", "").lower()
for entry in self.get_dev_dependencies(ecosystem):
if comp.group:
if comp.group.lower() != entry.get("namespace", ""):
continue

if comp.name.lower() == entry.get("name", ""):
return True
else:
# fallback: only check by name
dd = self.component_check_list.get("dev_dependencies", [])
for ecosystem in dd:
for entry in dd.get(ecosystem, []):
if comp.name.lower() == entry.get("name", ""):
return True

return False

def is_python_binary_component(self, comp: Component) -> bool:
"""Check whether the given component matches any known python component
with additional binary dependencies."""
if comp.purl:
pd = comp.purl.to_dict()
if pd.get("type", "").lower() != "pypi":
return False

pbc = self.component_check_list.get("python_binary_components", [])
for entry in pbc:
if comp.name.lower() == entry.get("name", ""):
return True

return False

def is_file_to_ignore(self, comp: Component) -> bool:
"""Check whether the given component is to be ignored."""
for entry in self.files_to_ignore:
if comp.name.lower() == entry.get("name", ""):
return True

return False

def check_bom_items(self, sbom: Bom) -> int:
"""Check the SBOM for special components."""
special_component_count = 0
for component in sbom.components:
if self.is_file_to_ignore(component):
continue

if self.is_dev_dependency(component):
special_component_count += 1
print_yellow(" ", component.name, component.version, "seems to be a development dependency")

if self.is_python_binary_component(component):
special_component_count += 1
print_yellow(" ", component.name, component.version,
"is known as a Python component that has additional binary dependencies")

return special_component_count

def run(self, args: Any) -> None:
"""Main method()"""
if args.debug:
global LOG
LOG = capycli.get_logger(__name__)

print_text(
"\n" + capycli.get_app_signature() +
" - Check the SBOM for special components.\n")

if args.help:
print("usage: CaPyCli bom componentcheck [-h] [-v] -i bomfile [-rcl URL] [-lcl FILE]")
print("")
print("optional arguments:")
print(" -h, --help show this help message and exit")
print(" -i INPUTFILE SBOM file to read from (JSON)")
print(" -v be verbose")
print(" -rcl read the component check list file from the URL specified")
print(" -lcl read the component check list file from local")
print(" --forceerror force an error exit code in case of validation errors or warnings")
return

if not args.inputfile:
print_red("No input file specified!")
sys.exit(ResultCode.RESULT_COMMAND_ERROR)

if not os.path.isfile(args.inputfile):
print_red("Input file not found!")
sys.exit(ResultCode.RESULT_FILE_NOT_FOUND)

print_text("Reading component check list from component_checks.json...")
try:
self.read_component_check_list(args.remote_check_list, args.local_checklist_list)
except Exception as ex:
print_red("Error reading component check list " + repr(ex))
sys.exit(ResultCode.RESULT_GENERAL_ERROR)
if len(self.component_check_list) > 0:
print(" Got component checklist.")

self.files_to_ignore = self.component_check_list.get("files_to_ignore", [])
print_text(f" {len(self.files_to_ignore)} components will be ignored.")

print_text("\nLoading SBOM file", args.inputfile)
try:
sbom = CaPyCliBom.read_sbom(args.inputfile)
# for c in sbom.components:
# print(c)
except Exception as ex:
print_red("Error reading SBOM: " + repr(ex))
sys.exit(ResultCode.RESULT_ERROR_READING_BOM)

if args.verbose:
print_text(" ", self.get_comp_count_text(sbom), "read from SBOM")

result = self.check_bom_items(sbom)
print_text("\nDone.")
if result > 0:
if args.force_error:
if args.verbose:
print_yellow("Special component(s) found, exiting with code != 0")
sys.exit(ResultCode.RESULT_SPECIAL_COMPONENT_FOUND)
10 changes: 9 additions & 1 deletion capycli/bom/handle_bom.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -------------------------------------------------------------------------------
# Copyright (c) 2019-2025 Siemens
# Copyright (c) 2019-2026 Siemens
# All Rights Reserved.
# Author: thomas.graf@siemens.com
#
Expand All @@ -15,6 +15,7 @@
import capycli.bom.check_bom
import capycli.bom.check_bom_item_status
import capycli.bom.check_granularity
import capycli.bom.component_check
import capycli.bom.create_components
import capycli.bom.diff_bom
import capycli.bom.download_sources
Expand Down Expand Up @@ -53,6 +54,7 @@ def run_bom_command(args: Any) -> None:
print(" Findsources determine the source code for SBOM items")
print(" Validate validate an SBOM")
print(" BomPackage create a single archive that contains the SBOM and all source and binary files")
print(" ComponentCheck check the SBOM for special components")
return

subcommand = args.command[1].lower()
Expand Down Expand Up @@ -147,5 +149,11 @@ def run_bom_command(args: Any) -> None:
app15.run(args)
return

if subcommand == "componentcheck":
"""Check the SBOM for special components."""
app16 = capycli.bom.component_check.ComponentCheck()
app16.run(args)
return

print_red("Unknown sub-command: ")
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
Loading
Loading