Repository: vmware/tern
Pull request #459 (2019-09-24 21:33:49)

Report package information by layer
Currently, Tern summarizes all the package names, versions and licenses
found throughout the container image at the end of the report
with no way to discern which layer the package and license information
was found in. This commit changes the output of the default
format to report the package + license information by image layer. It
also adds a summary section to the bottom of the report summarizing the
full list of licenses found throughout all the layers of the container
image. This separation of information between layers will be helpful
for larger container images with multiple layers.

Signed-off-by: Rose Judge <rjudge@vmware.com>

rnjudge: @nishakm This commit yields an ouput report that lists the packages and licenses by layer but also summarizes the full license list at the bottom of the report. I decided to keep the list of licenses found in each layer for a circumstance where a user might typically only look at the full license list, but then comes across a prohibited or concerning license in the summary. They may want to be able to go back and determine which layer pulls in that license.

Do you think this use case is too specific? If so, I can remove the licenses summary by layer and just keep the one at the end.
nishakm: > @nishakm This commit yields an ouput report that lists the packages and licenses by layer but also summarizes the full license list at the bottom of the report. I decided to keep the list of licenses found in each layer for a circumstance where a user might typically only look at the full license list, but then comes across a prohibited or concerning license in the summary. They may want to be able to go back and determine which layer pulls in that license.
> 
> Do you think this use case is too specific? If so, I can remove the licenses summary by layer and just keep the one at the end.

No, I think that's a good use case to have the license information repeated per layer.


Patch:
diff --git a/tern/classes/image_layer.py b/tern/classes/image_layer.py
index e859152..77e759e 100644
--- a/tern/classes/image_layer.py
+++ b/tern/classes/image_layer.py
@@ -29,6 +29,7 @@ class ImageLayer:
         import_str: The string from a build tool (like a Dockerfile) that
         created this layer by importing it from another image
         files_analyzed: whether the files in this layer are analyzed or not
+        analyzed_output: the result of the file analysis
     methods:
         add_package: adds a package to the layer
         remove_package: removes a package from the layer
@@ -47,6 +48,7 @@ class ImageLayer:
         self.__pkg_format = ''
         self.__os_guess = ''
         self.__files_analyzed = False
+        self.__analyzed_output = ''
 
     @property
     def diff_id(self):
@@ -108,6 +110,17 @@ class ImageLayer:
     def os_guess(self, os_guess):
         self.__os_guess = os_guess
 
+    @property
+    def analyzed_output(self):
+        return self.__analyzed_output
+
+    @analyzed_output.setter
+    def analyzed_output(self, analyzed_output):
+        if isinstance(analyzed_output, str):
+            self.__analyzed_output = analyzed_output
+        else:
+            raise ValueError('analyzed_output should be a string')
+    
     @property
     def files_analyzed(self):
         return self.__files_analyzed
@@ -118,7 +131,7 @@ class ImageLayer:
             self.__files_analyzed = x
         else:
             raise ValueError('files_analyzed should be boolean')
-    
+
     def add_package(self, package):
         if isinstance(package, Package):
             if package.name not in self.get_package_names():
diff --git a/tern/formats/default/generator.py b/tern/formats/default/generator.py
index 4f771e7..3c32ae9 100644
--- a/tern/formats/default/generator.py
+++ b/tern/formats/default/generator.py
@@ -23,8 +23,7 @@ def print_full_report(image):
     '''Given an image, go through the Origins object and collect all the
     notices for the image, layers and packages'''
     notes = ''
-    pkg_list = []
-    license_list = []
+    full_license_list = []
     for image_origin in image.origins.origins:
         notes = notes + content.print_notices(image_origin, '', '\t')
     for layer in image.layers:
@@ -34,19 +33,27 @@ def print_full_report(image):
             for layer_origin in layer.origins.origins:
                 notes = notes + content.print_notices(layer_origin,
                                                       '\t', '\t\t')
+            layer_pkg_list = []
+            layer_license_list = []
             for package in layer.packages:
                 pkg = package.name + "-" + package.version
-                if pkg not in pkg_list and pkg:
-                    pkg_list.append(pkg)
-                if package.pkg_license not in license_list and \
+                if pkg not in layer_pkg_list and pkg:
+                    layer_pkg_list.append(pkg)
+                if package.pkg_license not in layer_license_list and \
                         package.pkg_license:
-                    license_list.append(package.pkg_license)
+                    layer_license_list.append(package.pkg_license)
+                    if package.pkg_license not in full_license_list:
+                        full_license_list.append(package.pkg_license)
+            # Collect packages + licenses in the layer
+            notes = notes + formats.layer_packages_list.format(
+                list=", ".join(layer_pkg_list) if layer_pkg_list else 'None')
+            notes = notes + formats.layer_licenses_list.format(list=", ".join(
+                layer_license_list) if layer_license_list else 'None')
             notes = notes + formats.package_demarkation
-    packages = formats.packages_list.format(list=", ".join(pkg_list) if
-                                            pkg_list else 'None')
-    licenses = formats.licenses_list.format(list=", ".join(license_list) if
-                                            license_list else 'None')
-    return notes + packages + formats.package_demarkation + licenses
+    # Collect the full list of licenses from all the layers
+    licenses = formats.full_licenses_list.format(list=", ".join(
+        full_license_list) if full_license_list else 'None')
+    return notes + licenses
 
 
 class Default(generator.Generate):
diff --git a/tern/report/formats.py b/tern/report/formats.py
index c578bc5..0d03cbb 100644
--- a/tern/report/formats.py
+++ b/tern/report/formats.py
@@ -43,8 +43,11 @@ package_version = '''Version: {package_version}\n'''
 package_url = '''Project URL: {package_url}\n'''
 package_license = '''License: {package_license}\n'''
 package_copyright = '''Copyright Text: {package_copyright}\n'''
-packages_list = '''Packages Found:  {list}\n'''
-licenses_list = '''Licenses Found:  {list}\n'''
+layer_packages_list = '''\tPackages found in Layer:  {list}\n'''
+layer_licenses_list = '''\tLicenses found in Layer:  {list}\n'''
+full_licenses_list = '''###########################################\n'''\
+    '''# Summary of licenses found in Container: #\n'''\
+    '''###########################################\n{list}\n'''
 
 # notes
 package_notes = '''Errors: {package_info_retrieval_errors}\n''' \


Test patch:
diff --git a/tests/test_class_image_layer.py b/tests/test_class_image_layer.py
index 259ffdd..8946259 100644
--- a/tests/test_class_image_layer.py
+++ b/tests/test_class_image_layer.py
@@ -39,6 +39,11 @@ class TestClassImageLayer(unittest.TestCase):
         self.assertTrue(self.layer.files_analyzed)
         self.assertRaises(ValueError, setattr, self.layer,
                           'files_analyzed', 'some string')
+        self.assertEqual("", self.layer.analyzed_output)
+        self.layer.analyzed_output = 'some string'
+        self.assertEqual(self.layer.analyzed_output, 'some string')
+        self.assertRaises(ValueError, setattr, self.layer,
+                          'analyzed_output', 123)
 
     def testAddPackage(self):
         err = "Object type String, should be Package"