sbom4python 0.12.5__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.
@@ -0,0 +1,2 @@
1
+ # Copyright (C) 2023 Anthony Harrison
2
+ # SPDX-License-Identifier: Apache-2.0
sbom4python/cli.py ADDED
@@ -0,0 +1,215 @@
1
+ # Copyright (C) 2023 Anthony Harrison
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import argparse
5
+ import sys
6
+ import textwrap
7
+ from collections import ChainMap
8
+
9
+ from lib4sbom.generator import SBOMGenerator
10
+ from lib4sbom.output import SBOMOutput
11
+ from lib4sbom.sbom import SBOM
12
+ from sbom2dot.dotgenerator import DOTGenerator
13
+
14
+ from sbom4python.scanner import SBOMScanner
15
+ from sbom4python.version import VERSION
16
+
17
+ # CLI processing
18
+
19
+
20
+ def main(argv=None):
21
+
22
+ argv = argv or sys.argv
23
+ app_name = "sbom4python"
24
+ parser = argparse.ArgumentParser(
25
+ prog=app_name,
26
+ description=textwrap.dedent(
27
+ """
28
+ SBOM4Python generates a Software Bill of Materials for the
29
+ specified installed Python module identifying all of the dependent
30
+ components which are explicity defined (typically via requirements.txt
31
+ file) or implicitly as a hidden dependency.
32
+ """
33
+ ),
34
+ )
35
+ input_group = parser.add_argument_group("Input")
36
+ input_group.add_argument(
37
+ "-m",
38
+ "--module",
39
+ action="store",
40
+ default="",
41
+ help="identity of python module",
42
+ )
43
+ input_group.add_argument(
44
+ "-r",
45
+ "--requirement",
46
+ action="store",
47
+ default="",
48
+ help="name of requirements file",
49
+ )
50
+ input_group.add_argument(
51
+ "--system",
52
+ action="store_true",
53
+ help="include all installed python modules within system",
54
+ )
55
+ input_group.add_argument(
56
+ "--exclude-license",
57
+ action="store_true",
58
+ help="suppress detecting the license of components",
59
+ )
60
+ input_group.add_argument(
61
+ "--include-file",
62
+ action="store_true",
63
+ default=False,
64
+ help="include reporting files associated with module",
65
+ )
66
+ input_group.add_argument(
67
+ "--include-service",
68
+ action="store_true",
69
+ default=False,
70
+ help="include reporting of endpoints",
71
+ )
72
+ input_group.add_argument(
73
+ "--use-pip",
74
+ action="store_true",
75
+ default=False,
76
+ help="use pip for package management",
77
+ )
78
+ input_group.add_argument(
79
+ "--python",
80
+ action="store",
81
+ help="use specified Python interpreter for pip",
82
+ )
83
+
84
+ output_group = parser.add_argument_group("Output")
85
+ output_group.add_argument(
86
+ "-d",
87
+ "--debug",
88
+ action="store_true",
89
+ default=False,
90
+ help="add debug information",
91
+ )
92
+ output_group.add_argument(
93
+ "--sbom",
94
+ action="store",
95
+ default="spdx",
96
+ choices=["spdx", "cyclonedx"],
97
+ help="specify type of sbom to generate (default: spdx)",
98
+ )
99
+ output_group.add_argument(
100
+ "--format",
101
+ action="store",
102
+ default="tag",
103
+ choices=["tag", "json", "yaml"],
104
+ help="specify format of software bill of materials (sbom) (default: tag)",
105
+ )
106
+
107
+ output_group.add_argument(
108
+ "-o",
109
+ "--output-file",
110
+ action="store",
111
+ default="",
112
+ help="output filename (default: output to stdout)",
113
+ )
114
+
115
+ output_group.add_argument(
116
+ "-g",
117
+ "--graph",
118
+ action="store",
119
+ default="",
120
+ help="filename for dependency graph",
121
+ )
122
+
123
+ parser.add_argument("-V", "--version", action="version", version=VERSION)
124
+
125
+ defaults = {
126
+ "module": "",
127
+ "requirement": "",
128
+ "include_file": False,
129
+ "include_service": False,
130
+ "exclude_license": False,
131
+ "use_pip": False,
132
+ "system": False,
133
+ "output_file": "",
134
+ "sbom": "spdx",
135
+ "debug": False,
136
+ "format": "tag",
137
+ "graph": "",
138
+ "python": "",
139
+ }
140
+
141
+ raw_args = parser.parse_args(argv[1:])
142
+ args = {key: value for key, value in vars(raw_args).items() if value}
143
+ args = ChainMap(args, defaults)
144
+
145
+ # Validate CLI parameters
146
+
147
+ module_name = args["module"]
148
+
149
+ # Ensure format is aligned with type of SBOM
150
+ bom_format = args["format"]
151
+ if args["sbom"] == "cyclonedx":
152
+ # Only JSON format valid for CycloneDX
153
+ if bom_format != "json":
154
+ bom_format = "json"
155
+
156
+ if args["debug"]:
157
+ print("Exclude Licences:", args["exclude_license"])
158
+ print("Include Files:", args["include_file"])
159
+ print("Include Services:", args["include_service"])
160
+ print("Use Pip:", args["use_pip"])
161
+ print("Module", module_name)
162
+ print("Requirements file", args["requirement"])
163
+ print("System", args["system"])
164
+ print("SBOM type:", args["sbom"])
165
+ print("Format:", bom_format)
166
+ print("Output file:", args["output_file"])
167
+ print("Graph file:", args["graph"])
168
+ print(f"Analysing {module_name}")
169
+
170
+ sbom_scan = SBOMScanner(
171
+ args["debug"],
172
+ args["include_file"],
173
+ args["exclude_license"],
174
+ include_service=args["include_service"],
175
+ use_pip=args["use_pip"],
176
+ python_path=args["python"],
177
+ )
178
+
179
+ if len(module_name) > 0:
180
+ sbom_scan.process_python_module(module_name)
181
+ elif args["system"]:
182
+ sbom_scan.process_system()
183
+ elif len(args["requirement"]) > 0:
184
+ sbom_scan.process_requirements(args["requirement"])
185
+ else:
186
+ print("[ERROR] Nothing to process")
187
+ return -1
188
+
189
+ # Generate SBOM file
190
+ python_sbom = SBOM()
191
+ python_sbom.add_document(sbom_scan.get_document())
192
+ python_sbom.add_files(sbom_scan.get_files())
193
+ python_sbom.add_packages(sbom_scan.get_packages())
194
+ python_sbom.add_relationships(sbom_scan.get_relationships())
195
+
196
+ sbom_gen = SBOMGenerator(
197
+ sbom_type=args["sbom"], format=bom_format, application=app_name, version=VERSION
198
+ )
199
+ sbom_gen.generate(
200
+ project_name=sbom_scan.get_parent(),
201
+ sbom_data=python_sbom.get_sbom(),
202
+ filename=args["output_file"],
203
+ )
204
+
205
+ if len(args["graph"]) > 0:
206
+ sbom_dot = DOTGenerator(python_sbom.get_sbom()["packages"])
207
+ sbom_dot.generatedot(python_sbom.get_sbom()["relationships"])
208
+ dot_out = SBOMOutput(args["graph"], "dot")
209
+ dot_out.generate_output(sbom_dot.getDOT())
210
+
211
+ return 0
212
+
213
+
214
+ if __name__ == "__main__":
215
+ sys.exit(main())
sbom4python/license.py ADDED
@@ -0,0 +1,54 @@
1
+ # Copyright (C) 2023 Anthony Harrison
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
5
+ import json
6
+ import os
7
+
8
+
9
+ class LicenseScanner:
10
+
11
+ APACHE_SYNOYMNS = [
12
+ "Apache Software License",
13
+ "Apache License, Version 2.0",
14
+ "Apache 2.0",
15
+ "Apache_2.0",
16
+ "Apache 2",
17
+ ]
18
+ DEFAULT_LICENSE = "UNKNOWN"
19
+ SPDX_LICENSE_VERSION = "3.18"
20
+
21
+ def __init__(self):
22
+ # Load licenses
23
+ license_dir, filename = os.path.split(__file__)
24
+ license_path = os.path.join(license_dir, "license_data", "spdx_licenses.json")
25
+ licfile = open(license_path)
26
+ self.licenses = json.load(licfile)
27
+
28
+ def get_license_version(self):
29
+ return self.SPDX_LICENSE_VERSION
30
+
31
+ def check_synoymn(self, license, synoymns, value):
32
+ return value if license in synoymns else None
33
+
34
+ def find_license(self, license):
35
+ # Search list of licenses to find match
36
+
37
+ for lic in self.licenses["licenses"]:
38
+ # Comparisons ignore case of provided license text
39
+ if lic["licenseId"].lower() == license.lower():
40
+ return lic["licenseId"]
41
+ elif lic["name"].lower() == license.lower():
42
+ return lic["licenseId"]
43
+ license_id = self.check_synoymn(license, self.APACHE_SYNOYMNS, "Apache-2.0")
44
+ return license_id if license_id is not None else self.DEFAULT_LICENSE
45
+
46
+ def get_license_url(self, license_id):
47
+ # Assume that license_id is a valid SPDX id
48
+ if license_id != self.DEFAULT_LICENSE:
49
+ for lic in self.licenses["licenses"]:
50
+ # License URL is in the seeAlso field.
51
+ # If multiple entries, just return first one
52
+ if lic["licenseId"] == license_id:
53
+ return lic["seeAlso"][0]
54
+ return None # License not found