Skip to content

Commit b45e584

Browse files
authored
Add package supplier info to Tern reports
This commit adds package supplier information as an attribute in the package class for package objects. This commit also adds `pkg_supplier` attribute values as `PackageSupplier` field values in Tag Value and JSON SPDX documents . For some package managers (like PyPI), there is only one feasible supplier (PyPI) and this value is set as a constant string (PyPI). For others, like rpm, the string is determined using the `/etc/os-release` file based on the Linux Distro providing the packages. While this is not a perfect way to determine the distro/distributor, it is satisfactory to satisfy the NTIA minimum elements for the upcoming EO 14028. We decided to use the distro as the supplier based on conversations had on the SPDX mailing list[1]. [1] https://lists.spdx.org/g/Spdx-tech/message/4942 Resolves #1205 Signed-off-by: Rose Judge <[email protected]>
2 parents 2e51f67 + ede4645 commit b45e584

File tree

12 files changed

+83
-5
lines changed

12 files changed

+83
-5
lines changed

tern/analyze/default/bundle.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ def convert_to_pkg_dicts(attr_lists):
5252
'pkg_licenses': 'pkg_licenses',
5353
'files': 'files',
5454
'src_name': 'source_names',
55-
'src_version': 'source_versions'}
55+
'src_version': 'source_versions',
56+
'pkg_supplier': 'pkg_suppliers'}
5657
pkg_list = []
5758
len_names = len(attr_lists['names'])
5859
# make a list of keys that correspond with package property names

tern/analyze/default/command_lib/base.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ dpkg:
8888
container:
8989
- "dpkg-query -W -f '${Version}\n'"
9090
delimiter: "\n"
91+
pkg_suppliers:
92+
invoke:
93+
1:
94+
container:
95+
- "distro=`/bin/cat /etc/os-release | grep NAME | sed -n '2p' | cut -f 2 -d '=' | cut -d '\"' -f2`"
96+
- "pkgs=`dpkg-query -W -f '${Package}\n'`"
97+
- "for p in $pkgs; do echo $distro; done"
98+
delimiter: "\n"
9199
source_names:
92100
invoke:
93101
1:
@@ -148,6 +156,13 @@ apk:
148156
- "pkgs=`apk info 2>/dev/null`"
149157
- "for p in $pkgs; do lic=`apk info $p 2>/dev/null | head -1 | awk '{print $1}'`; echo $lic | sed -e \"s/^$p-//\"; done"
150158
delimiter: "\n"
159+
pkg_suppliers:
160+
invoke:
161+
1:
162+
container:
163+
- "pkgs=`apk info 2>/dev/null`"
164+
- "for p in $pkgs; do echo 'Alpine Linux'; done"
165+
delimiter: "\n"
151166
licenses:
152167
invoke:
153168
1:
@@ -192,6 +207,13 @@ pacman:
192207
container:
193208
- 'pacman -Q 2>/dev/null | cut -f 2 -d " "'
194209
delimiter: "\n"
210+
pkg_suppliers:
211+
invoke:
212+
1:
213+
container:
214+
- 'pkgs=`pacman -Qq 2>/dev/null`'
215+
- 'for p in $pkgs; do echo "Arch Linux"; done'
216+
delimiter: "\n"
195217
licenses:
196218
invoke:
197219
1:
@@ -238,6 +260,14 @@ rpm:
238260
container:
239261
- 'rpm --query --all --queryformat "%{version}\n" 2>/dev/null'
240262
delimiter: "\n"
263+
pkg_suppliers:
264+
invoke:
265+
1:
266+
container:
267+
- "distro=`/bin/cat /etc/os-release | grep NAME | sed -n '1p' | cut -f 2 -d '=' | cut -d '\"' -f2`"
268+
- "pkgs=`rpm --query --all --queryformat \"%{name}\n\" 2>/dev/null`"
269+
- "for p in $pkgs; do echo $distro; done"
270+
delimiter: "\n"
241271
licenses:
242272
invoke:
243273
1:
@@ -302,6 +332,13 @@ pip:
302332
- "pkgs=`pip list --format=freeze 2> /dev/null | cut -f1 -d'='`"
303333
- "for p in $pkgs; do pip show $p 2> /dev/null | head -2 | tail -1 | cut -f2 -d' '; done"
304334
delimiter: "\n"
335+
pkg_suppliers:
336+
invoke:
337+
1:
338+
container:
339+
- "pkgs=`pip list --format=freeze 2> /dev/null | cut -f1 -d'='`"
340+
- "for p in $pkgs; do echo 'PyPI'; done"
341+
delimiter: "\n"
305342
licenses:
306343
invoke:
307344
1:
@@ -345,6 +382,13 @@ pip3:
345382
- "pkgs=`pip3 list --format=freeze 2> /dev/null | cut -f1 -d'='`"
346383
- "for p in $pkgs; do pip3 show $p 2> /dev/null | head -2 | tail -1 | cut -f2 -d' '; done"
347384
delimiter: "\n"
385+
pkg_suppliers:
386+
invoke:
387+
1:
388+
container:
389+
- "pkgs=`pip3 list --format=freeze 2> /dev/null | cut -f1 -d'='`"
390+
- "for p in $pkgs; do echo 'PyPI'; done"
391+
delimiter: "\n"
348392
licenses:
349393
invoke:
350394
1:

tern/analyze/default/command_lib/command_lib.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@
3737
command_lib['snippets'] = yaml.safe_load(f)
3838
# list of package information keys that the command library can accomodate
3939
base_keys = {'names', 'versions', 'licenses', 'source_names',
40-
'source_versions', 'copyrights', 'proj_urls', 'srcs', 'files'}
40+
'source_versions', 'pkg_suppliers', 'copyrights',
41+
'proj_urls', 'srcs', 'files'}
4142
package_keys = {'name', 'version', 'license', 'src_name', 'src_version',
42-
'copyright', 'proj_url', 'src', 'files'}
43+
'pkg_supplier', 'copyright', 'proj_url', 'src', 'files'}
4344

4445
# global logger
4546
logger = logging.getLogger(constants.logger_name)

tern/classes/package.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class Package:
2424
pkg_licenses: all licenses found within a package
2525
src_name: the source package associated with the binary package
2626
src_version: the source package version
27+
pkg_supplier: the package distributor according to SPDX 7.5;
28+
required to create NTIA compliant SPDX docs
2729
2830
methods:
2931
to_dict: returns a dict representation of the instance
@@ -47,6 +49,7 @@ def __init__(self, name):
4749
self.__pkg_format = ''
4850
self.__src_name = ''
4951
self.__src_version = ''
52+
self.__pkg_supplier = ''
5053

5154
@property
5255
def name(self):
@@ -144,6 +147,14 @@ def src_version(self):
144147
def src_version(self, src_version):
145148
self.__src_version = src_version
146149

150+
@property
151+
def pkg_supplier(self):
152+
return self.__pkg_supplier
153+
154+
@pkg_supplier.setter
155+
def pkg_supplier(self, pkg_supplier):
156+
self.__pkg_supplier = pkg_supplier
157+
147158
def get_file_paths(self):
148159
"""Return a list of paths of all the files in a package"""
149160
return [f.path for f in self.__files]
@@ -217,6 +228,7 @@ def fill(self, package_dict):
217228
files: <package files>
218229
src_name: <source package>
219230
src_ver: <source package version>
231+
pkg_supplier: <package distributor/supplier>
220232
the way to use this method is to instantiate the class with the
221233
name and then give it a package dictionary to fill in the rest
222234
return true if package name is the same as the one used to instantiate

tern/formats/spdx/spdx.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ def package(self):
2121
'copyright': 'PackageCopyrightText',
2222
'download_url': 'PackageDownloadLocation',
2323
'src_name': 'SourcePackageName',
24-
'src_version': 'SourcePackageVersion'}
24+
'src_version': 'SourcePackageVersion',
25+
'pkg_supplier': 'PackageSupplier'}
2526

2627
def image_layer(self):
2728
return {'tar_file': 'PackageFileName'}

tern/formats/spdx/spdxjson/image_helpers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def get_image_dict(image_obj, template):
8484
'name': mapping['PackageName'],
8585
'SPDXID': spdx_common.get_image_spdxref(image_obj),
8686
'versionInfo': mapping['PackageVersion'],
87+
'supplier': 'NOASSERTION', # always NOASSERTION
8788
'downloadLocation': 'NOASSERTION', # always NOASSERTION
8889
'filesAnalyzed': False, # always false
8990
'licenseConcluded': 'NOASSERTION', # always NOASSERTION

tern/formats/spdx/spdxjson/layer_helpers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ def get_layer_dict(layer_obj):
150150
'name': os.path.basename(layer_obj.tar_file),
151151
'SPDXID': spdx_common.get_layer_spdxref(layer_obj),
152152
'versionInfo': layer_obj.layer_index,
153+
'supplier': 'NOASSERTION', # always NOASSERTION
153154
'packageFileName': layer_obj.tar_file,
154155
'downloadLocation': 'NONE',
155156
'filesAnalyzed': bool(layer_obj.files_analyzed),

tern/formats/spdx/spdxjson/package_helpers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,14 @@ def get_package_dict(package, template):
5858
JSON dictionary representation of the package. The analyzed files will
5959
go in a separate dictionary for the JSON document.'''
6060
mapping = package.to_dict(template)
61+
supplier_str = 'Organization: ' + mapping['PackageSupplier']
6162
pkg_ref, _ = spdx_common.get_package_spdxref(package)
6263
package_dict = {
6364
'name': mapping['PackageName'],
6465
'SPDXID': pkg_ref,
6566
'versionInfo': mapping['PackageVersion'] if mapping['PackageVersion']
6667
else 'NOASSERTION',
68+
'supplier': supplier_str if mapping['PackageSupplier'] else 'NOASSERTION',
6769
'downloadLocation': mapping['PackageDownloadLocation'] if
6870
mapping['PackageDownloadLocation'] else 'NOASSERTION',
6971
'filesAnalyzed': False, # always false for packages

tern/formats/spdx/spdxtagvalue/image_helpers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ def get_image_block(image_obj, template):
101101
block += 'SPDXID: {}\n'.format(spdx_common.get_image_spdxref(image_obj))
102102
# Package Version
103103
block += 'PackageVersion: {}\n'.format(mapping['PackageVersion'])
104+
# Package Supplier (always NOASSERTION)
105+
block += 'PackageSupplier: NOASSERTION\n'
104106
# Package Download Location (always NOASSERTION)
105107
block += 'PackageDownloadLocation: NOASSERTION\n'
106108
# Files Analyzed (always false)

tern/formats/spdx/spdxtagvalue/layer_helpers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ def get_layer_block(layer_obj, template):
117117
block += 'SPDXID: {}\n'.format(spdx_common.get_layer_spdxref(layer_obj))
118118
# Package Version. For Layer objects, this is just the layer_index
119119
block += 'PackageVersion: {}\n'.format(layer_obj.layer_index)
120+
# Package Supplier (always NOASSERTION)
121+
block += 'PackageSupplier: NOASSERTION\n'
120122
# Package File Name
121123
block += 'PackageFileName: {}\n'.format(layer_obj.tar_file)
122124
# Package Download Location (always NONE for layers)

0 commit comments

Comments
 (0)