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.
Files changed (78) hide show
  1. License.md +27 -0
  2. capycli/__init__.py +214 -0
  3. capycli/__main__.py +13 -0
  4. capycli/bom/__init__.py +10 -0
  5. capycli/bom/bom_convert.py +163 -0
  6. capycli/bom/check_bom.py +187 -0
  7. capycli/bom/check_bom_item_status.py +197 -0
  8. capycli/bom/check_granularity.py +244 -0
  9. capycli/bom/create_components.py +644 -0
  10. capycli/bom/csv.py +69 -0
  11. capycli/bom/diff_bom.py +279 -0
  12. capycli/bom/download_sources.py +227 -0
  13. capycli/bom/filter_bom.py +323 -0
  14. capycli/bom/findsources.py +278 -0
  15. capycli/bom/handle_bom.py +134 -0
  16. capycli/bom/html.py +67 -0
  17. capycli/bom/legacy.py +312 -0
  18. capycli/bom/legacy_cx.py +151 -0
  19. capycli/bom/map_bom.py +1039 -0
  20. capycli/bom/merge_bom.py +155 -0
  21. capycli/bom/plaintext.py +69 -0
  22. capycli/bom/show_bom.py +77 -0
  23. capycli/common/__init__.py +9 -0
  24. capycli/common/capycli_bom_support.py +629 -0
  25. capycli/common/comparable_version.py +161 -0
  26. capycli/common/component_cache.py +240 -0
  27. capycli/common/dependencies_base.py +48 -0
  28. capycli/common/file_support.py +28 -0
  29. capycli/common/html_support.py +119 -0
  30. capycli/common/json_support.py +36 -0
  31. capycli/common/map_result.py +116 -0
  32. capycli/common/print.py +55 -0
  33. capycli/common/purl_service.py +169 -0
  34. capycli/common/purl_store.py +100 -0
  35. capycli/common/purl_utils.py +85 -0
  36. capycli/common/script_base.py +165 -0
  37. capycli/common/script_support.py +78 -0
  38. capycli/data/__init__.py +9 -0
  39. capycli/data/granularity_list.csv +1338 -0
  40. capycli/dependencies/__init__.py +9 -0
  41. capycli/dependencies/handle_dependencies.py +70 -0
  42. capycli/dependencies/javascript.py +261 -0
  43. capycli/dependencies/maven_list.py +333 -0
  44. capycli/dependencies/maven_pom.py +150 -0
  45. capycli/dependencies/nuget.py +184 -0
  46. capycli/dependencies/python.py +345 -0
  47. capycli/main/__init__.py +9 -0
  48. capycli/main/application.py +165 -0
  49. capycli/main/argument_parser.py +101 -0
  50. capycli/main/cli.py +28 -0
  51. capycli/main/exceptions.py +14 -0
  52. capycli/main/options.py +424 -0
  53. capycli/main/result_codes.py +41 -0
  54. capycli/mapping/handle_mapping.py +46 -0
  55. capycli/mapping/mapping_to_html.py +182 -0
  56. capycli/mapping/mapping_to_xlsx.py +197 -0
  57. capycli/moverview/handle_moverview.py +46 -0
  58. capycli/moverview/moverview_to_html.py +122 -0
  59. capycli/moverview/moverview_to_xlsx.py +170 -0
  60. capycli/project/__init__.py +9 -0
  61. capycli/project/check_prerequisites.py +304 -0
  62. capycli/project/create_bom.py +190 -0
  63. capycli/project/create_project.py +335 -0
  64. capycli/project/create_readme.py +546 -0
  65. capycli/project/find_project.py +128 -0
  66. capycli/project/get_license_info.py +246 -0
  67. capycli/project/handle_project.py +118 -0
  68. capycli/project/show_ecc.py +200 -0
  69. capycli/project/show_licenses.py +211 -0
  70. capycli/project/show_project.py +215 -0
  71. capycli/project/show_vulnerabilities.py +238 -0
  72. capycli-2.0.0.dev8.dist-info/LICENSES/CC0-1.0.txt +121 -0
  73. capycli-2.0.0.dev8.dist-info/LICENSES/MIT.txt +27 -0
  74. capycli-2.0.0.dev8.dist-info/License.md +27 -0
  75. capycli-2.0.0.dev8.dist-info/METADATA +268 -0
  76. capycli-2.0.0.dev8.dist-info/RECORD +78 -0
  77. capycli-2.0.0.dev8.dist-info/WHEEL +4 -0
  78. capycli-2.0.0.dev8.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,197 @@
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_red, print_text
22
+ from capycli.main.result_codes import ResultCode
23
+
24
+ LOG = capycli.get_logger(__name__)
25
+
26
+
27
+ class CheckBomItemStatus(capycli.common.script_base.ScriptBase):
28
+ """Print SBOM item status to stdout"""
29
+
30
+ def _bom_has_items_without_id(self, bom: Bom) -> bool:
31
+ """Determines whether there is at least one SBOM item
32
+ without Sw360Id."""
33
+ for item in bom.components:
34
+ sw360id = CycloneDxSupport.get_property_value(item, CycloneDxSupport.CDX_PROP_SW360ID)
35
+ if not sw360id:
36
+ return True
37
+
38
+ return False
39
+
40
+ def _find_by_id(self, component: Component) -> dict | None:
41
+ sw360id = CycloneDxSupport.get_property_value(component, CycloneDxSupport.CDX_PROP_SW360ID)
42
+ try:
43
+ release_details = self.client.get_release(sw360id)
44
+ return release_details
45
+ except sw360.sw360_api.SW360Error as swex:
46
+ if swex.response.status_code == requests.codes['not_found']:
47
+ print(
48
+ Fore.LIGHTYELLOW_EX + " Not found " + component.name +
49
+ ", " + component.version + ", " +
50
+ sw360id + Style.RESET_ALL)
51
+ else:
52
+ print(Fore.LIGHTRED_EX + " Error retrieving release data: ")
53
+ print(
54
+ " " + str(component.name) + ", " + str(component.version) +
55
+ ", " + sw360id)
56
+ print(" Status Code: " + str(swex.response.status_code))
57
+ if swex.message:
58
+ print(" Message: " + swex.message)
59
+ print(Style.RESET_ALL)
60
+
61
+ return None
62
+
63
+ def _find_by_name(self, component: Component) -> dict | None:
64
+ try:
65
+ releases = self.client.get_releases_by_name(component.name)
66
+ if not releases:
67
+ return None
68
+
69
+ for r in releases:
70
+ if r.get("version", "") == component.version:
71
+ return self.client.get_release_by_url(r["_links"]["self"]["href"])
72
+
73
+ return None
74
+ except sw360.sw360_api.SW360Error as swex:
75
+ if swex.response.status_code == requests.codes['not_found']:
76
+ print(
77
+ Fore.LIGHTYELLOW_EX + " Not found " + component.name +
78
+ ", " + component.version + ", " +
79
+ Style.RESET_ALL)
80
+ else:
81
+ print(Fore.LIGHTRED_EX + " Error retrieving release data: ")
82
+ print(" " + str(component.name) + ", " + str(component.version))
83
+ print(" Status Code: " + str(swex.response.status_code))
84
+ if swex.message:
85
+ print(" Message: " + swex.message)
86
+ print(Style.RESET_ALL)
87
+
88
+ return None
89
+
90
+ def show_bom_item_status(self, bom: Bom, all: bool = False) -> None:
91
+ for component in bom.components:
92
+ release = None
93
+ id = CycloneDxSupport.get_property_value(component, CycloneDxSupport.CDX_PROP_SW360ID)
94
+ if id:
95
+ release = self._find_by_id(component)
96
+ else:
97
+ release = self._find_by_name(component)
98
+
99
+ if release:
100
+ if not all:
101
+ cs = release.get("clearingState", "(unknown clearing state)")
102
+ color = Fore.WHITE
103
+ if cs == "APPROVED":
104
+ color = Fore.LIGHTGREEN_EX
105
+
106
+ print(
107
+ color +
108
+ " " + component.name + ", " + component.version +
109
+ " => " + cs + ", " +
110
+ release.get("mainlineState", "(unknown mainline state)") +
111
+ Style.RESET_ALL)
112
+ continue
113
+
114
+ comp_sw360 = self.client.get_component(
115
+ self.client.get_id_from_href(
116
+ release["_links"]["sw360:component"]["href"]
117
+ )
118
+ )
119
+
120
+ rel_list = comp_sw360["_embedded"]["sw360:releases"]
121
+ print(" " + component.name + ", " + component.version + " => ", end="", flush=True)
122
+ print("releases for component found = " + str(len(rel_list)))
123
+ for orel in rel_list:
124
+ href = orel["_links"]["self"]["href"]
125
+ rel = self.client.get_release_by_url(href)
126
+ cs = rel.get("clearingState", "(unkown clearing state)")
127
+ if cs == "APPROVED":
128
+ print(Fore.LIGHTGREEN_EX, end="", flush=True)
129
+ print(
130
+ " " + orel["version"] + ", " + cs + ", " +
131
+ rel.get("mainlineState", "(unknown mainline state)"))
132
+ print(Style.RESET_ALL, end="", flush=True)
133
+
134
+ print("")
135
+ continue
136
+
137
+ if not id:
138
+ print_red(
139
+ " " + component.name + ", " + component.version +
140
+ " => --- no id ---")
141
+ continue
142
+
143
+ def run(self, args) -> None:
144
+ """Main method()"""
145
+ if args.debug:
146
+ global LOG
147
+ LOG = capycli.get_logger(__name__)
148
+ else:
149
+ # suppress (debug) log output from requests and urllib
150
+ logging.getLogger("requests").setLevel(logging.WARNING)
151
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
152
+ logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
153
+
154
+ print_text(
155
+ "\n" + capycli.APP_NAME + ", " + capycli.get_app_version()
156
+ + " - check the status of the items on SW360\n")
157
+
158
+ if args.help:
159
+ print("usage: capycli bom CheckItemStatus [-h] [-all] -i bomfile")
160
+ print("")
161
+ print("optional arguments:")
162
+ print("-h, --help show this help message and exit")
163
+ print("-i INPUTFILE input file to read from")
164
+ print("-all show status of all versions of the component")
165
+ return
166
+
167
+ if not args.inputfile:
168
+ print_red("No input file specified!")
169
+ sys.exit(ResultCode.RESULT_COMMAND_ERROR)
170
+
171
+ if not os.path.isfile(args.inputfile):
172
+ print_red("Input file not found!")
173
+ sys.exit(ResultCode.RESULT_FILE_NOT_FOUND)
174
+
175
+ print("Loading SBOM file", args.inputfile)
176
+ try:
177
+ bom = CaPyCliBom.read_sbom(args.inputfile)
178
+ except Exception as ex:
179
+ print_red("Error reading SBOM: " + repr(ex))
180
+ sys.exit(ResultCode.RESULT_ERROR_READING_BOM)
181
+
182
+ if args.verbose:
183
+ print_text(" ", self.get_comp_count_text(bom), " read from SBOM")
184
+
185
+ if self._bom_has_items_without_id(bom):
186
+ print("There are SBOM items without Sw360 id - searching per name may take a little bit longer...")
187
+
188
+ if args.sw360_token and args.oauth2:
189
+ self.analyze_token(args.sw360_token)
190
+
191
+ if not self.login(token=args.sw360_token, url=args.sw360_url, oauth2=args.oauth2):
192
+ print_red("ERROR: login failed!")
193
+ sys.exit(ResultCode.RESULT_AUTH_ERROR)
194
+
195
+ self.show_bom_item_status(bom, args.all)
196
+
197
+ print()
@@ -0,0 +1,244 @@
1
+ # -------------------------------------------------------------------------------
2
+ # Copyright (c) 2021-2023 Siemens
3
+ # All Rights Reserved.
4
+ # Author: thomas.graf@siemens.com
5
+ #
6
+ # SPDX-License-Identifier: MIT
7
+ # -------------------------------------------------------------------------------
8
+
9
+ import importlib.resources
10
+ import os
11
+ import sys
12
+
13
+ from cyclonedx.model import ExternalReferenceType
14
+ from cyclonedx.model.bom import Bom
15
+ from cyclonedx.model.component import Component
16
+ from packageurl import PackageURL
17
+ from sortedcontainers import SortedSet
18
+
19
+ import capycli.common.json_support
20
+ import capycli.common.script_base
21
+ from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport, SbomWriter
22
+ from capycli.common.print import print_red, print_text, print_yellow
23
+ from capycli.dependencies.javascript import GetJavascriptDependencies
24
+ from capycli.main.result_codes import ResultCode
25
+
26
+ LOG = capycli.get_logger(__name__)
27
+
28
+
29
+ class PotentialGranularityIssue:
30
+ """Class to hold potential granularity issues."""
31
+ def __init__(self, component, replacement, comment="", source_url=""):
32
+ self.component = component
33
+ self.replacement = replacement
34
+ self.comment = comment
35
+ self.source_url = source_url
36
+
37
+
38
+ class CheckGranularity(capycli.common.script_base.ScriptBase):
39
+ """
40
+ Check the granularity of all releases in the SBOM.
41
+ """
42
+ def __init__(self):
43
+ self.granularity_list = []
44
+
45
+ def read_granularity_list(self) -> None:
46
+ """Reads the granularity list from file."""
47
+ self.granularity_list = []
48
+
49
+ # read CSV from data resource
50
+ resources = importlib.resources.files("capycli.data")
51
+ text_list = (resources / "granularity_list.csv").read_text()
52
+ for line in text_list.splitlines():
53
+ # ignore header (first) line
54
+ if line.startswith("component_name;replacement_name"):
55
+ continue
56
+
57
+ # ignore comments
58
+ if line.startswith("#"):
59
+ continue
60
+
61
+ line = line.strip()
62
+ if not line:
63
+ continue
64
+
65
+ # split line
66
+ parts = line.split(";")
67
+ if len(parts) < 2:
68
+ continue
69
+
70
+ component = parts[0]
71
+ replacement = parts[1]
72
+ comment = ""
73
+ if len(parts) > 2:
74
+ comment = parts[2]
75
+
76
+ source_url = ""
77
+ if len(parts) > 3:
78
+ source_url = parts[3]
79
+
80
+ issue = PotentialGranularityIssue(component, replacement, comment, source_url)
81
+ self.granularity_list.append(issue)
82
+
83
+ def find_match(self, name: str) -> PotentialGranularityIssue or None:
84
+ """Finds a match by component name."""
85
+ for match in self.granularity_list:
86
+ if match.component.lower() == name.lower():
87
+ return match
88
+
89
+ return None
90
+
91
+ def get_new_fixed_component(self, component: Component, new_name: str, new_src_url: str) -> Component:
92
+ """Get a !NEW! CycloneDX component to replace the old one."""
93
+ source_url_bak = CycloneDxSupport.get_ext_ref_source_url(component)
94
+ if new_src_url:
95
+ source_url_bak = new_src_url
96
+ language_bak = CycloneDxSupport.get_property(component, CycloneDxSupport.CDX_PROP_LANGUAGE)
97
+
98
+ # build new package-url
99
+ purl = ""
100
+ if component.purl:
101
+ old_purl = PackageURL.from_string(component.purl)
102
+ purl = PackageURL(old_purl.type, old_purl.namespace, new_name, component.version).to_string()
103
+
104
+ if self.search_meta_data:
105
+ if str(component.purl).startswith("pkg:npm"):
106
+ GetJavascriptDependencies().try_find_component_metadata(component, "")
107
+ else:
108
+ LOG.warning(" No package-url available - creating default purl")
109
+ purl = PackageURL("generic", "", new_name, component.version).to_string()
110
+
111
+ # create new component (this is the only way to set a new bom_ref)
112
+ component_new = Component(
113
+ name=new_name,
114
+ version=component.version,
115
+ purl=purl,
116
+ bom_ref=purl
117
+ )
118
+
119
+ # restore properties we can keep
120
+ if source_url_bak:
121
+ CycloneDxSupport.update_or_set_ext_ref(
122
+ component_new,
123
+ ExternalReferenceType.DISTRIBUTION,
124
+ CaPyCliBom.SOURCE_URL_COMMENT,
125
+ source_url_bak
126
+ )
127
+
128
+ if language_bak:
129
+ component_new.properties.add(language_bak)
130
+
131
+ if component.purl and self.search_meta_data:
132
+ if str(component.purl).startswith("pkg:npm"):
133
+ component_new = GetJavascriptDependencies().try_find_component_metadata(component_new, "")
134
+
135
+ return component_new
136
+
137
+ def merge_duplicates(self, clist: list[Component]) -> list[Component]:
138
+ """Checks for each release if there are duplicates after granularity check."""
139
+ new_list = []
140
+ for release in clist:
141
+ count = len([item for item in new_list if item.name == release.name
142
+ and item.version == release.version])
143
+ if count > 0:
144
+ continue
145
+ else:
146
+ new_list.append(release)
147
+
148
+ print()
149
+ print_text(str(len(clist) - len(new_list)) + " items can be reduced by granularity check")
150
+
151
+ return new_list
152
+
153
+ def check_bom_items(self, sbom: Bom):
154
+ """Checks for each release in the list whether it can be found on the specified
155
+ SW360 instance."""
156
+
157
+ new_comp_list = []
158
+ for component in sbom.components:
159
+ match = self.find_match(component.name)
160
+ if not match:
161
+ new_comp_list.append(component)
162
+ continue
163
+
164
+ print_yellow(
165
+ component.name + ", " +
166
+ component.version + " should get replaced by " +
167
+ match.replacement)
168
+
169
+ new_component = self.get_new_fixed_component(
170
+ component,
171
+ match.replacement,
172
+ match.source_url)
173
+
174
+ new_comp_list.append(new_component)
175
+
176
+ reduced = self.merge_duplicates(new_comp_list)
177
+ sbom.components = SortedSet(reduced)
178
+ return sbom
179
+
180
+ def run(self, args):
181
+ """Main method()"""
182
+ if args.debug:
183
+ global LOG
184
+ LOG = capycli.get_logger(__name__)
185
+
186
+ print_text(
187
+ "\n" + capycli.APP_NAME + ", " + capycli.get_app_version() +
188
+ " - Check the granularity of all releases in the SBOM.\n")
189
+
190
+ if args.help:
191
+ print("usage: CaPyCli bom granularity [-h] [-v] -i bomfile -o updated")
192
+ print("")
193
+ print("optional arguments:")
194
+ print(" -h, --help show this help message and exit")
195
+ print(" -i INPUTFILE SBOM file to read from (JSON)")
196
+ print(" -o OUTPUTFILE write updated to this file (optinal)")
197
+ print(" -v be verbose")
198
+ return
199
+
200
+ if not args.inputfile:
201
+ print_red("No input file specified!")
202
+ sys.exit(ResultCode.RESULT_COMMAND_ERROR)
203
+
204
+ if not os.path.isfile(args.inputfile):
205
+ print_red("Input file not found!")
206
+ sys.exit(ResultCode.RESULT_FILE_NOT_FOUND)
207
+
208
+ print_text("Reading granularity data from granularity_list.csv...")
209
+ try:
210
+ self.read_granularity_list()
211
+ except Exception as ex:
212
+ print_red("Error reading granularity data: " + repr(ex))
213
+ sys.exit(ResultCode.RESULT_GENERAL_ERROR)
214
+ print(" " + str(len(self.granularity_list)) + " items read.")
215
+
216
+ print_text("\nLoading SBOM file", args.inputfile)
217
+ try:
218
+ sbom = CaPyCliBom.read_sbom(args.inputfile)
219
+ except Exception as ex:
220
+ print_red("Error reading SBOM: " + repr(ex))
221
+ sys.exit(ResultCode.RESULT_ERROR_READING_BOM)
222
+
223
+ if args.verbose:
224
+ print_text(" ", self.get_comp_count_text(sbom), " read from SBOM")
225
+
226
+ self.search_meta_data = args.search_meta_data
227
+
228
+ new_sbom = self.check_bom_items(sbom)
229
+
230
+ print()
231
+ if args.outputfile:
232
+ print_text("Writing new SBOM to " + args.outputfile)
233
+
234
+ try:
235
+ SbomWriter.write_to_json(new_sbom, args.outputfile, True)
236
+ except Exception as ex:
237
+ print_red("Error writing new SBOM: " + repr(ex))
238
+ sys.exit(ResultCode.RESULT_ERROR_WRITING_BOM)
239
+
240
+ print_text(" " + self.get_comp_count_text(new_sbom) + " written to file " + args.outputfile)
241
+ else:
242
+ print_text("To get updated SBOM file - use the '-o <filename>' parameter")
243
+
244
+ print_text("\nDone.")