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.
- sbom4python/__init__.py +2 -0
- sbom4python/cli.py +215 -0
- sbom4python/license.py +54 -0
- sbom4python/license_data/spdx_licenses.json +6258 -0
- sbom4python/scanner.py +788 -0
- sbom4python/version.py +4 -0
- sbom4python-0.12.5.dist-info/LICENSE +201 -0
- sbom4python-0.12.5.dist-info/METADATA +192 -0
- sbom4python-0.12.5.dist-info/RECORD +14 -0
- sbom4python-0.12.5.dist-info/WHEEL +5 -0
- sbom4python-0.12.5.dist-info/entry_points.txt +2 -0
- sbom4python-0.12.5.dist-info/top_level.txt +1 -0
- test/__init__.py +2 -0
- test/test_license.py +62 -0
sbom4python/__init__.py
ADDED
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
|