capycli 2.0.0.dev8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- License.md +27 -0
- capycli/__init__.py +214 -0
- capycli/__main__.py +13 -0
- capycli/bom/__init__.py +10 -0
- capycli/bom/bom_convert.py +163 -0
- capycli/bom/check_bom.py +187 -0
- capycli/bom/check_bom_item_status.py +197 -0
- capycli/bom/check_granularity.py +244 -0
- capycli/bom/create_components.py +644 -0
- capycli/bom/csv.py +69 -0
- capycli/bom/diff_bom.py +279 -0
- capycli/bom/download_sources.py +227 -0
- capycli/bom/filter_bom.py +323 -0
- capycli/bom/findsources.py +278 -0
- capycli/bom/handle_bom.py +134 -0
- capycli/bom/html.py +67 -0
- capycli/bom/legacy.py +312 -0
- capycli/bom/legacy_cx.py +151 -0
- capycli/bom/map_bom.py +1039 -0
- capycli/bom/merge_bom.py +155 -0
- capycli/bom/plaintext.py +69 -0
- capycli/bom/show_bom.py +77 -0
- capycli/common/__init__.py +9 -0
- capycli/common/capycli_bom_support.py +629 -0
- capycli/common/comparable_version.py +161 -0
- capycli/common/component_cache.py +240 -0
- capycli/common/dependencies_base.py +48 -0
- capycli/common/file_support.py +28 -0
- capycli/common/html_support.py +119 -0
- capycli/common/json_support.py +36 -0
- capycli/common/map_result.py +116 -0
- capycli/common/print.py +55 -0
- capycli/common/purl_service.py +169 -0
- capycli/common/purl_store.py +100 -0
- capycli/common/purl_utils.py +85 -0
- capycli/common/script_base.py +165 -0
- capycli/common/script_support.py +78 -0
- capycli/data/__init__.py +9 -0
- capycli/data/granularity_list.csv +1338 -0
- capycli/dependencies/__init__.py +9 -0
- capycli/dependencies/handle_dependencies.py +70 -0
- capycli/dependencies/javascript.py +261 -0
- capycli/dependencies/maven_list.py +333 -0
- capycli/dependencies/maven_pom.py +150 -0
- capycli/dependencies/nuget.py +184 -0
- capycli/dependencies/python.py +345 -0
- capycli/main/__init__.py +9 -0
- capycli/main/application.py +165 -0
- capycli/main/argument_parser.py +101 -0
- capycli/main/cli.py +28 -0
- capycli/main/exceptions.py +14 -0
- capycli/main/options.py +424 -0
- capycli/main/result_codes.py +41 -0
- capycli/mapping/handle_mapping.py +46 -0
- capycli/mapping/mapping_to_html.py +182 -0
- capycli/mapping/mapping_to_xlsx.py +197 -0
- capycli/moverview/handle_moverview.py +46 -0
- capycli/moverview/moverview_to_html.py +122 -0
- capycli/moverview/moverview_to_xlsx.py +170 -0
- capycli/project/__init__.py +9 -0
- capycli/project/check_prerequisites.py +304 -0
- capycli/project/create_bom.py +190 -0
- capycli/project/create_project.py +335 -0
- capycli/project/create_readme.py +546 -0
- capycli/project/find_project.py +128 -0
- capycli/project/get_license_info.py +246 -0
- capycli/project/handle_project.py +118 -0
- capycli/project/show_ecc.py +200 -0
- capycli/project/show_licenses.py +211 -0
- capycli/project/show_project.py +215 -0
- capycli/project/show_vulnerabilities.py +238 -0
- capycli-2.0.0.dev8.dist-info/LICENSES/CC0-1.0.txt +121 -0
- capycli-2.0.0.dev8.dist-info/LICENSES/MIT.txt +27 -0
- capycli-2.0.0.dev8.dist-info/License.md +27 -0
- capycli-2.0.0.dev8.dist-info/METADATA +268 -0
- capycli-2.0.0.dev8.dist-info/RECORD +78 -0
- capycli-2.0.0.dev8.dist-info/WHEEL +4 -0
- capycli-2.0.0.dev8.dist-info/entry_points.txt +3 -0
License.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
# SPDX-FileCopyrightText: (c) 2018-2023 Siemens
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2019-2023 Siemens
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
11
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
12
|
+
the Software without restriction, including without limitation the rights to
|
|
13
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
14
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
15
|
+
so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice (including the next
|
|
18
|
+
paragraph) shall be included in all copies or substantial portions of the
|
|
19
|
+
Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
capycli/__init__.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# -------------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2019-23 Siemens
|
|
3
|
+
# All Rights Reserved.
|
|
4
|
+
# Author: thomas.graf@siemens.com
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: MIT
|
|
7
|
+
# SPDX-FileCopyrightText: (c) 2019-2023 Siemens
|
|
8
|
+
# -------------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
"""Top-level module for CaPyCli.
|
|
11
|
+
|
|
12
|
+
This module
|
|
13
|
+
- initializes logging for the command-line tool
|
|
14
|
+
- tracks the version of the package
|
|
15
|
+
- provides a way to configure logging for the command-line tool
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import importlib
|
|
19
|
+
import logging
|
|
20
|
+
import sys
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import tomli
|
|
24
|
+
from colorama import Fore, Style, init
|
|
25
|
+
|
|
26
|
+
APP_NAME = "CaPyCli"
|
|
27
|
+
VERBOSITY_LEVEL = 1
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_debug_logging_enabled() -> bool:
|
|
31
|
+
return VERBOSITY_LEVEL > 1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_project_meta() -> Any:
|
|
35
|
+
"""Read version information from poetry configuration file."""
|
|
36
|
+
try:
|
|
37
|
+
with open('pyproject.toml', mode='rb') as pyproject:
|
|
38
|
+
return tomli.load(pyproject)['tool']['poetry']
|
|
39
|
+
except Exception:
|
|
40
|
+
# ignore all errors
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_app_version() -> str:
|
|
45
|
+
"""Get the version string of this application"""
|
|
46
|
+
version = ""
|
|
47
|
+
try:
|
|
48
|
+
# this will only work when the package has been installed
|
|
49
|
+
version = importlib.metadata.version("capycli")
|
|
50
|
+
except: # noqa
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
if not version:
|
|
54
|
+
# use version information from poetry
|
|
55
|
+
pkg_meta = _get_project_meta()
|
|
56
|
+
version = str(pkg_meta['version'])
|
|
57
|
+
|
|
58
|
+
if not version:
|
|
59
|
+
version = "0.0.0-no-version"
|
|
60
|
+
|
|
61
|
+
return version
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# There is nothing lower than logging.DEBUG (10) in the logging library,
|
|
65
|
+
# but we want an extra level to avoid being too verbose when using -vv.
|
|
66
|
+
_EXTRA_VERBOSE = 5
|
|
67
|
+
logging.addLevelName(_EXTRA_VERBOSE, "VERBOSE")
|
|
68
|
+
|
|
69
|
+
_VERBOSITY_TO_LOG_LEVEL = {
|
|
70
|
+
# output more than warnings but not debugging info
|
|
71
|
+
1: logging.INFO, # INFO is a numerical level of 20
|
|
72
|
+
# output debugging information
|
|
73
|
+
2: logging.DEBUG, # DEBUG is a numerical level of 10
|
|
74
|
+
# output extra verbose debugging information
|
|
75
|
+
3: _EXTRA_VERBOSE,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# initialize colorama
|
|
79
|
+
init()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ConsoleHandler(logging.Handler):
|
|
83
|
+
"""Handler that write to stderr for errors and to stdout
|
|
84
|
+
for other logiing records."""
|
|
85
|
+
def __init__(self) -> None:
|
|
86
|
+
super().__init__()
|
|
87
|
+
|
|
88
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
89
|
+
"""Emit a record."""
|
|
90
|
+
try:
|
|
91
|
+
msg = self.format(record)
|
|
92
|
+
if record.levelno >= 40:
|
|
93
|
+
# error, critical
|
|
94
|
+
sys.stderr.write(msg)
|
|
95
|
+
elif record.levelno >= 30:
|
|
96
|
+
# warning
|
|
97
|
+
sys.stderr.write(msg)
|
|
98
|
+
print(msg)
|
|
99
|
+
else:
|
|
100
|
+
# info, debug, all other
|
|
101
|
+
print(msg)
|
|
102
|
+
except Exception:
|
|
103
|
+
self.handleError(record)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ColorFormatter(logging.Formatter):
|
|
107
|
+
"""
|
|
108
|
+
A logging formatter for color cosole output.
|
|
109
|
+
Critical messages and errors are displayed in red.
|
|
110
|
+
Warnings are displayed in yellow.
|
|
111
|
+
Infos are displayed in white.
|
|
112
|
+
Debug messages are displayed in blue.
|
|
113
|
+
"""
|
|
114
|
+
def __init__(self, verbosity: int) -> None:
|
|
115
|
+
super().__init__()
|
|
116
|
+
self.verbosity = verbosity
|
|
117
|
+
self.fmt = "%(asctime)s:%(levelname)s:%(name)s: %(message)s"
|
|
118
|
+
if self.verbosity == 1:
|
|
119
|
+
self.fmt = "%(message)s"
|
|
120
|
+
|
|
121
|
+
def get_color_format(self, levelno: int, fmt: str) -> Any:
|
|
122
|
+
if levelno >= 50:
|
|
123
|
+
color = Fore.LIGHTRED_EX
|
|
124
|
+
elif levelno >= 40:
|
|
125
|
+
color = Fore.LIGHTRED_EX
|
|
126
|
+
elif levelno >= 30:
|
|
127
|
+
color = Fore.LIGHTYELLOW_EX
|
|
128
|
+
elif levelno >= 20:
|
|
129
|
+
color = Fore.LIGHTWHITE_EX
|
|
130
|
+
elif levelno >= 10:
|
|
131
|
+
color = Fore.LIGHTBLUE_EX
|
|
132
|
+
else:
|
|
133
|
+
color = Fore.WHITE
|
|
134
|
+
|
|
135
|
+
return color + fmt + Style.RESET_ALL
|
|
136
|
+
|
|
137
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
138
|
+
log_fmt = self.get_color_format(record.levelno, self.fmt)
|
|
139
|
+
formatter = logging.Formatter(log_fmt)
|
|
140
|
+
return formatter.format(record)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ColoredLogger(logging.Logger):
|
|
144
|
+
"""
|
|
145
|
+
A color console logger that uses ColorFormatter
|
|
146
|
+
to display colored log messages and uses ConsoleHandler
|
|
147
|
+
to output critical messages, errors and warnings to stderr.
|
|
148
|
+
Infos and debug messages will be sent to stdout.
|
|
149
|
+
"""
|
|
150
|
+
def __init__(self, name: str):
|
|
151
|
+
logging.Logger.__init__(self, name, logging.DEBUG)
|
|
152
|
+
|
|
153
|
+
self.propagate = False
|
|
154
|
+
self.setVerbosity(1)
|
|
155
|
+
|
|
156
|
+
def getVerbosity(self) -> int:
|
|
157
|
+
return self.__verbosity
|
|
158
|
+
|
|
159
|
+
def setVerbosity(self, value: int) -> None:
|
|
160
|
+
self.__verbosity = value
|
|
161
|
+
console = ConsoleHandler()
|
|
162
|
+
color_formatter = ColorFormatter(self.__verbosity)
|
|
163
|
+
console.setFormatter(color_formatter)
|
|
164
|
+
self.handlers.clear()
|
|
165
|
+
self.addHandler(console)
|
|
166
|
+
|
|
167
|
+
def handle(self, record: logging.LogRecord) -> None:
|
|
168
|
+
if self.isEnabledFor(record.levelno):
|
|
169
|
+
return super().handle(record)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def configure_logging(verbosity: int) -> logging.Logger:
|
|
173
|
+
"""
|
|
174
|
+
Configure logging.
|
|
175
|
+
|
|
176
|
+
:param int verbosity:
|
|
177
|
+
How verbose to be in logging information.
|
|
178
|
+
"""
|
|
179
|
+
logging.setLoggerClass(ColoredLogger)
|
|
180
|
+
|
|
181
|
+
global VERBOSITY_LEVEL
|
|
182
|
+
VERBOSITY_LEVEL = verbosity
|
|
183
|
+
|
|
184
|
+
if verbosity < 0:
|
|
185
|
+
verbosity = 0
|
|
186
|
+
if verbosity > 3:
|
|
187
|
+
verbosity = 3
|
|
188
|
+
|
|
189
|
+
log_level = _VERBOSITY_TO_LOG_LEVEL[verbosity]
|
|
190
|
+
|
|
191
|
+
# log_level = logging.DEBUG # too much output from other libraries
|
|
192
|
+
logging.basicConfig(level=log_level)
|
|
193
|
+
|
|
194
|
+
logger = logging.getLogger(__name__)
|
|
195
|
+
logger.setVerbosity(verbosity) # type: ignore
|
|
196
|
+
logger.setLevel(log_level)
|
|
197
|
+
|
|
198
|
+
global LOG
|
|
199
|
+
LOG = logger
|
|
200
|
+
|
|
201
|
+
return logger
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def get_logger(name: str) -> logging.Logger:
|
|
205
|
+
"""Get one of our colored loggers for the specified name."""
|
|
206
|
+
logger = logging.getLogger(name)
|
|
207
|
+
logger.setVerbosity(VERBOSITY_LEVEL) # type: ignore
|
|
208
|
+
log_level = _VERBOSITY_TO_LOG_LEVEL[VERBOSITY_LEVEL]
|
|
209
|
+
logger.setLevel(log_level)
|
|
210
|
+
return logger
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# Initialize logging
|
|
214
|
+
LOG = configure_logging(1)
|
capycli/__main__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# -------------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2019-23 Siemens
|
|
3
|
+
# All Rights Reserved.
|
|
4
|
+
# Author: thomas.graf@siemens.com
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: MIT
|
|
7
|
+
# SPDX-FileCopyrightText: (c) 2019-2023 Siemens
|
|
8
|
+
# -------------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
"""Module allowing for ``python -m CaPyCli ...``."""
|
|
11
|
+
from capycli.main import cli
|
|
12
|
+
|
|
13
|
+
cli.main()
|
capycli/bom/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# -------------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2019-23 Siemens
|
|
3
|
+
# All Rights Reserved.
|
|
4
|
+
# Author: thomas.graf@siemens.com
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: MIT
|
|
7
|
+
# SPDX-FileCopyrightText: (c) 2018-2023 Siemens
|
|
8
|
+
# -------------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
"""SBOM specific methods"""
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# -------------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2023 Siemens
|
|
3
|
+
# All Rights Reserved.
|
|
4
|
+
# Author: thomas.graf@siemens.com
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: MIT
|
|
7
|
+
# -------------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
# from enum import StrEnum # not supported in Python 3.10.3
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import capycli.common.json_support
|
|
17
|
+
import capycli.common.script_base
|
|
18
|
+
from capycli import get_logger
|
|
19
|
+
from capycli.common.capycli_bom_support import CaPyCliBom
|
|
20
|
+
from capycli.common.print import print_red, print_text
|
|
21
|
+
from capycli.main.exceptions import CaPyCliException
|
|
22
|
+
from capycli.main.result_codes import ResultCode
|
|
23
|
+
|
|
24
|
+
from .csv import CsvSupport
|
|
25
|
+
from .html import HtmlConversionSupport
|
|
26
|
+
from .legacy import LegacySupport
|
|
27
|
+
from .legacy_cx import LegacyCx
|
|
28
|
+
from .plaintext import PlainTextSupport
|
|
29
|
+
|
|
30
|
+
LOG = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BomFormat(str, Enum):
|
|
34
|
+
# CaPyCLI flavor of Siemens Standard BOM/CycloneDX
|
|
35
|
+
CAPYCLI = "capycli"
|
|
36
|
+
# Siemens Standard BOM
|
|
37
|
+
SBOM = "sbom"
|
|
38
|
+
# plain text
|
|
39
|
+
TEXT = "text"
|
|
40
|
+
# CSV
|
|
41
|
+
CSV = "csv"
|
|
42
|
+
# CaPyCLI JSON
|
|
43
|
+
LEGACY = "legacy"
|
|
44
|
+
# CaPyCLI CycloneDX
|
|
45
|
+
LEGACY_CX = "legacy-cx"
|
|
46
|
+
# HTML
|
|
47
|
+
HTML = "html"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BomConvert(capycli.common.script_base.ScriptBase):
|
|
51
|
+
def convert(self,
|
|
52
|
+
inputfile: str,
|
|
53
|
+
inputformat: str,
|
|
54
|
+
outputfile: str,
|
|
55
|
+
outputformat: str) -> None:
|
|
56
|
+
"""Main conversion method."""
|
|
57
|
+
if not outputformat:
|
|
58
|
+
# default is CaPyCLI
|
|
59
|
+
outputformat = BomFormat.CAPYCLI
|
|
60
|
+
|
|
61
|
+
cdx_components = []
|
|
62
|
+
project = None
|
|
63
|
+
sbom = None
|
|
64
|
+
try:
|
|
65
|
+
if inputformat == BomFormat.TEXT:
|
|
66
|
+
cdx_components = PlainTextSupport.flatlist_to_cdx_components(inputfile)
|
|
67
|
+
print_text(f" {len(cdx_components)} components read from file {inputfile}")
|
|
68
|
+
elif inputformat == BomFormat.CSV:
|
|
69
|
+
cdx_components = CsvSupport.csv_to_cdx_components(inputfile)
|
|
70
|
+
print_text(f" {len(cdx_components)} components read from file {inputfile}")
|
|
71
|
+
elif (inputformat == BomFormat.CAPYCLI) or (inputformat == BomFormat.SBOM):
|
|
72
|
+
sbom = CaPyCliBom.read_sbom(inputfile)
|
|
73
|
+
cdx_components = sbom.components
|
|
74
|
+
project = sbom.metadata.component
|
|
75
|
+
print_text(f" {len(cdx_components)} components read from file {inputfile}")
|
|
76
|
+
elif inputformat == BomFormat.LEGACY:
|
|
77
|
+
cdx_components = LegacySupport.legacy_to_cdx_components(inputfile)
|
|
78
|
+
print_text(f" {len(cdx_components)} components read from file {inputfile}")
|
|
79
|
+
elif inputformat == BomFormat.LEGACY_CX:
|
|
80
|
+
sbom = LegacyCx.read_sbom(inputfile)
|
|
81
|
+
cdx_components = sbom.components
|
|
82
|
+
print_text(f" {len(cdx_components)} components read from file {inputfile}")
|
|
83
|
+
else:
|
|
84
|
+
print_red("Unsupported input format!")
|
|
85
|
+
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
|
|
86
|
+
except CaPyCliException as error:
|
|
87
|
+
LOG.error(f"Error processing input file: {str(error)}")
|
|
88
|
+
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
if outputformat == BomFormat.TEXT:
|
|
92
|
+
PlainTextSupport.write_cdx_components_as_flatlist(cdx_components, outputfile)
|
|
93
|
+
print_text(f" {len(cdx_components)} components written to file {outputfile}")
|
|
94
|
+
elif outputformat == BomFormat.CSV:
|
|
95
|
+
CsvSupport.write_cdx_components_as_csv(cdx_components, outputfile)
|
|
96
|
+
print_text(f" {len(cdx_components)} components written to file {outputfile}")
|
|
97
|
+
elif outputformat == BomFormat.HTML:
|
|
98
|
+
HtmlConversionSupport.write_cdx_components_as_html(cdx_components, outputfile, project)
|
|
99
|
+
print_text(f" {len(cdx_components)} components written to file {outputfile}")
|
|
100
|
+
elif outputformat == BomFormat.CAPYCLI:
|
|
101
|
+
if sbom:
|
|
102
|
+
CaPyCliBom.write_sbom(sbom, outputfile)
|
|
103
|
+
print_text(f" {len(sbom.components)} components written to file {outputfile}")
|
|
104
|
+
else:
|
|
105
|
+
CaPyCliBom.write_simple_sbom(cdx_components, outputfile)
|
|
106
|
+
print_text(f" {len(cdx_components)} components written to file {outputfile}")
|
|
107
|
+
elif outputformat == BomFormat.LEGACY:
|
|
108
|
+
LegacySupport.write_cdx_components_as_legacy(cdx_components, outputfile)
|
|
109
|
+
print_text(f" {len(cdx_components)} components written to file {outputfile}")
|
|
110
|
+
else:
|
|
111
|
+
LOG.error("Unsupported output format!")
|
|
112
|
+
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
|
|
113
|
+
except CaPyCliException as error:
|
|
114
|
+
LOG.error(f"Error creating output file: {str(error)}")
|
|
115
|
+
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
|
|
116
|
+
|
|
117
|
+
def check_arguments(self, args: Any) -> None:
|
|
118
|
+
"""Check input arguments."""
|
|
119
|
+
if not args.inputfile:
|
|
120
|
+
LOG.error("No input file specified!")
|
|
121
|
+
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
|
|
122
|
+
|
|
123
|
+
if not os.path.isfile(args.inputfile):
|
|
124
|
+
LOG.error("Input file not found!")
|
|
125
|
+
sys.exit(ResultCode.RESULT_FILE_NOT_FOUND)
|
|
126
|
+
|
|
127
|
+
if not args.inputformat:
|
|
128
|
+
LOG.error("No input format specified!")
|
|
129
|
+
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
|
|
130
|
+
|
|
131
|
+
if not args.outputfile:
|
|
132
|
+
LOG.error("No output file specified!")
|
|
133
|
+
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
|
|
134
|
+
|
|
135
|
+
if not args.outputformat:
|
|
136
|
+
LOG.warning("No output format specified, defaulting to sbom")
|
|
137
|
+
|
|
138
|
+
def display_help(self) -> None:
|
|
139
|
+
"""Display (local) help."""
|
|
140
|
+
print("usage: CaPyCli bom convert [-h] [-i INPUTFILE] [-if {capycli,text,csv,legacy,legacy-cx}]")
|
|
141
|
+
print(" [-o OUTPUTFILE] [-of {capycli,text,csv,legacy,legacy-cx,html}]")
|
|
142
|
+
print("")
|
|
143
|
+
print("optional arguments:")
|
|
144
|
+
print(" -h, --help Show this help message and exit")
|
|
145
|
+
print(" -i INPUTFILE Input BOM filename (JSON)")
|
|
146
|
+
print(" -o OUTPUTFILE Output BOM filename")
|
|
147
|
+
print(" -if INPUTFORMAT Specify input file format: capycli|sbom|text|csv|legacy|legacy-cx")
|
|
148
|
+
print(" -of OUTPUTFORMAT Specify output file format: capycli|text|csv|legacy|html")
|
|
149
|
+
|
|
150
|
+
def run(self, args):
|
|
151
|
+
"""Main method()"""
|
|
152
|
+
print("\n" + capycli.APP_NAME + ", " + capycli.get_app_version() + " - Convert SBOM formats\n")
|
|
153
|
+
|
|
154
|
+
if args.help:
|
|
155
|
+
self.display_help()
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
self.check_arguments(args)
|
|
159
|
+
if args.debug:
|
|
160
|
+
global LOG
|
|
161
|
+
LOG = get_logger(__name__)
|
|
162
|
+
|
|
163
|
+
self.convert(args.inputfile, args.inputformat, args.outputfile, args.outputformat)
|
capycli/bom/check_bom.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# -------------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2019-23 Siemens
|
|
3
|
+
# All Rights Reserved.
|
|
4
|
+
# Author: thomas.graf@siemens.com
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: MIT
|
|
7
|
+
# -------------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
import sw360.sw360_api
|
|
15
|
+
from colorama import Fore, Style
|
|
16
|
+
from cyclonedx.model.bom import Bom
|
|
17
|
+
from cyclonedx.model.component import Component
|
|
18
|
+
|
|
19
|
+
import capycli.common.script_base
|
|
20
|
+
from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport
|
|
21
|
+
from capycli.common.print import print_green, print_red, print_text, print_yellow
|
|
22
|
+
from capycli.main.result_codes import ResultCode
|
|
23
|
+
|
|
24
|
+
LOG = capycli.get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CheckBom(capycli.common.script_base.ScriptBase):
|
|
28
|
+
"""
|
|
29
|
+
Check that all releases listed in the SBOM really exist
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def _bom_has_items_without_id(self, bom: Bom) -> bool:
|
|
33
|
+
"""Determines whether there is at least one SBOM item
|
|
34
|
+
without Sw360Id."""
|
|
35
|
+
for item in bom.components:
|
|
36
|
+
sw360id = CycloneDxSupport.get_property_value(item, CycloneDxSupport.CDX_PROP_SW360ID)
|
|
37
|
+
if not sw360id:
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
def _find_by_id(self, component: Component) -> dict:
|
|
43
|
+
sw360id = CycloneDxSupport.get_property_value(component, CycloneDxSupport.CDX_PROP_SW360ID)
|
|
44
|
+
for step in range(3):
|
|
45
|
+
try:
|
|
46
|
+
release_details = self.client.get_release(sw360id)
|
|
47
|
+
return release_details
|
|
48
|
+
except sw360.sw360_api.SW360Error as swex:
|
|
49
|
+
if swex.response.status_code == requests.codes['not_found']:
|
|
50
|
+
print_yellow(
|
|
51
|
+
" Not found " + component.name +
|
|
52
|
+
", " + component.version + ", " + sw360id)
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
# only report other errors if this is the third attempt
|
|
56
|
+
if step >= 2:
|
|
57
|
+
print(Fore.LIGHTRED_EX + " Error retrieving release data: ")
|
|
58
|
+
print(
|
|
59
|
+
" " + component.name + ", " + component.version +
|
|
60
|
+
", " + sw360id)
|
|
61
|
+
print(" Status Code: " + str(swex.response.status_code))
|
|
62
|
+
if swex.message:
|
|
63
|
+
print(" Message: " + swex.message)
|
|
64
|
+
print(Style.RESET_ALL)
|
|
65
|
+
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
def _find_by_name(self, component: Component) -> dict:
|
|
69
|
+
for step in range(3):
|
|
70
|
+
try:
|
|
71
|
+
releases = self.client.get_releases_by_name(component.name)
|
|
72
|
+
if not releases:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
for r in releases:
|
|
76
|
+
if r.get("version", "") == component.version:
|
|
77
|
+
return r
|
|
78
|
+
|
|
79
|
+
return None
|
|
80
|
+
except sw360.sw360_api.SW360Error as swex:
|
|
81
|
+
if swex.response.status_code == requests.codes['not_found']:
|
|
82
|
+
print_yellow(
|
|
83
|
+
" Not found " + component.name +
|
|
84
|
+
", " + component.version)
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
# only report other errors if this is the third attempt
|
|
88
|
+
if step >= 2:
|
|
89
|
+
print(Fore.LIGHTRED_EX + " Error retrieving release data: ")
|
|
90
|
+
print(
|
|
91
|
+
" " + component.name + ", " + component.version)
|
|
92
|
+
print(" Status Code: " + str(swex.response.status_code))
|
|
93
|
+
if swex.message:
|
|
94
|
+
print(" Message: " + swex.message)
|
|
95
|
+
print(Style.RESET_ALL)
|
|
96
|
+
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def check_releases(self, bom: Bom) -> int:
|
|
100
|
+
"""Checks for each release in the list whether it can be found on the specified
|
|
101
|
+
SW360 instance."""
|
|
102
|
+
found_count = 0
|
|
103
|
+
for component in bom.components:
|
|
104
|
+
release_details = None
|
|
105
|
+
sw360id = CycloneDxSupport.get_property_value(component, CycloneDxSupport.CDX_PROP_SW360ID)
|
|
106
|
+
if sw360id:
|
|
107
|
+
release_details = self._find_by_id(component)
|
|
108
|
+
else:
|
|
109
|
+
release_details = self._find_by_name(component)
|
|
110
|
+
|
|
111
|
+
if release_details:
|
|
112
|
+
sid = self.client.get_id_from_href(release_details["_links"]["self"]["href"])
|
|
113
|
+
print_green(
|
|
114
|
+
" Found " + release_details["name"] +
|
|
115
|
+
", " + release_details["version"] + ", " + sid)
|
|
116
|
+
found_count += 1
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
if not id:
|
|
120
|
+
print_yellow(
|
|
121
|
+
" " + component.name +
|
|
122
|
+
", " + component.version +
|
|
123
|
+
" - No id available - skipping!")
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
return found_count
|
|
127
|
+
|
|
128
|
+
def run(self, args):
|
|
129
|
+
"""Main method()"""
|
|
130
|
+
if args.debug:
|
|
131
|
+
global LOG
|
|
132
|
+
LOG = capycli.get_logger(__name__)
|
|
133
|
+
else:
|
|
134
|
+
# suppress (debug) log output from requests and urllib
|
|
135
|
+
logging.getLogger("requests").setLevel(logging.WARNING)
|
|
136
|
+
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
|
137
|
+
logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
|
|
138
|
+
|
|
139
|
+
print_text(
|
|
140
|
+
"\n" + capycli.APP_NAME + ", " + capycli.get_app_version() +
|
|
141
|
+
" - Check that all releases in the SBOM exist on target SW360 instance.\n")
|
|
142
|
+
|
|
143
|
+
if args.help:
|
|
144
|
+
print("usage: CaPyCli bom check [-h] [-t SW360_TOKEN] [-oa] [-url SW360_URL] [-v] -i bomfile")
|
|
145
|
+
print("")
|
|
146
|
+
print("optional arguments:")
|
|
147
|
+
print(" -h, --help show this help message and exit")
|
|
148
|
+
print(" -t SW360_TOKEN, SW360_TOKEN")
|
|
149
|
+
print(" use this token for access to SW360")
|
|
150
|
+
print(" -oa, --oauth2 this is an oauth2 token")
|
|
151
|
+
print(" -url SW360_URL use this URL for access to SW360")
|
|
152
|
+
print(" -i INPUTFILE SBOM file to read from")
|
|
153
|
+
print(" -v be verbose")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
if not args.inputfile:
|
|
157
|
+
print_red("No input file specified!")
|
|
158
|
+
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
|
|
159
|
+
|
|
160
|
+
if not os.path.isfile(args.inputfile):
|
|
161
|
+
print_red("Input file not found!")
|
|
162
|
+
sys.exit(ResultCode.RESULT_FILE_NOT_FOUND)
|
|
163
|
+
|
|
164
|
+
print("Loading SBOM file", args.inputfile)
|
|
165
|
+
try:
|
|
166
|
+
bom = CaPyCliBom.read_sbom(args.inputfile)
|
|
167
|
+
except Exception as ex:
|
|
168
|
+
print_red("Error loading SBOM: " + repr(ex))
|
|
169
|
+
sys.exit(ResultCode.RESULT_ERROR_READING_BOM)
|
|
170
|
+
|
|
171
|
+
if args.verbose:
|
|
172
|
+
print_text(" ", self.get_comp_count_text(bom), " read from SBOM")
|
|
173
|
+
|
|
174
|
+
if self._bom_has_items_without_id(bom):
|
|
175
|
+
print("There are SBOM items without Sw360 id - searching per name may take a little bit longer...")
|
|
176
|
+
|
|
177
|
+
if args.sw360_token and args.oauth2:
|
|
178
|
+
self.analyze_token(args.sw360_token)
|
|
179
|
+
|
|
180
|
+
if not self.login(token=args.sw360_token, url=args.sw360_url, oauth2=args.oauth2):
|
|
181
|
+
print_red("ERROR: login failed!")
|
|
182
|
+
sys.exit(ResultCode.RESULT_AUTH_ERROR)
|
|
183
|
+
|
|
184
|
+
found = self.check_releases(bom)
|
|
185
|
+
|
|
186
|
+
print()
|
|
187
|
+
print(len(bom.components), "components checked,", found, "successfully found.")
|