1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: MIT
5#
6
7import json
8import os
9from oeqa.selftest.case import OESelftestTestCase
10from oeqa.utils.commands import bitbake, get_bb_vars
11
12class CVECheck(OESelftestTestCase):
13
14    def test_version_compare(self):
15        from oe.cve_check import Version
16
17        result = Version("100") > Version("99")
18        self.assertTrue( result, msg="Failed to compare version '100' > '99'")
19        result = Version("2.3.1") > Version("2.2.3")
20        self.assertTrue( result, msg="Failed to compare version '2.3.1' > '2.2.3'")
21        result = Version("2021-01-21") > Version("2020-12-25")
22        self.assertTrue( result, msg="Failed to compare version '2021-01-21' > '2020-12-25'")
23        result = Version("1.2-20200910") < Version("1.2-20200920")
24        self.assertTrue( result, msg="Failed to compare version '1.2-20200910' < '1.2-20200920'")
25
26        result = Version("1.0") >= Version("1.0beta")
27        self.assertTrue( result, msg="Failed to compare version '1.0' >= '1.0beta'")
28        result = Version("1.0-rc2") > Version("1.0-rc1")
29        self.assertTrue( result, msg="Failed to compare version '1.0-rc2' > '1.0-rc1'")
30        result = Version("1.0.alpha1") < Version("1.0")
31        self.assertTrue( result, msg="Failed to compare version '1.0.alpha1' < '1.0'")
32        result = Version("1.0_dev") <= Version("1.0")
33        self.assertTrue( result, msg="Failed to compare version '1.0_dev' <= '1.0'")
34
35        # ignore "p1" and "p2", so these should be equal
36        result = Version("1.0p2") == Version("1.0p1")
37        self.assertTrue( result ,msg="Failed to compare version '1.0p2' to '1.0p1'")
38        # ignore the "b" and "r"
39        result = Version("1.0b") == Version("1.0r")
40        self.assertTrue( result ,msg="Failed to compare version '1.0b' to '1.0r'")
41
42        # consider the trailing alphabet as patched level when comparing
43        result = Version("1.0b","alphabetical") < Version("1.0r","alphabetical")
44        self.assertTrue( result ,msg="Failed to compare version with suffix '1.0b' < '1.0r'")
45        result = Version("1.0b","alphabetical") > Version("1.0","alphabetical")
46        self.assertTrue( result ,msg="Failed to compare version with suffix '1.0b' > '1.0'")
47
48        # consider the trailing "p" and "patch" as patched released when comparing
49        result = Version("1.0","patch") < Version("1.0p1","patch")
50        self.assertTrue( result ,msg="Failed to compare version with suffix '1.0' < '1.0p1'")
51        result = Version("1.0p2","patch") > Version("1.0p1","patch")
52        self.assertTrue( result ,msg="Failed to compare version with suffix '1.0p2' > '1.0p1'")
53        result = Version("1.0_patch2","patch") < Version("1.0_patch3","patch")
54        self.assertTrue( result ,msg="Failed to compare version with suffix '1.0_patch2' < '1.0_patch3'")
55
56
57    def test_recipe_report_json(self):
58        config = """
59INHERIT += "cve-check"
60CVE_CHECK_FORMAT_JSON = "1"
61"""
62        self.write_config(config)
63
64        vars = get_bb_vars(["CVE_CHECK_SUMMARY_DIR", "CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
65        summary_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], vars["CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
66        recipe_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], "m4-native_cve.json")
67
68        try:
69            os.remove(summary_json)
70            os.remove(recipe_json)
71        except FileNotFoundError:
72            pass
73
74        bitbake("m4-native -c cve_check")
75
76        def check_m4_json(filename):
77            with open(filename) as f:
78                report = json.load(f)
79            self.assertEqual(report["version"], "1")
80            self.assertEqual(len(report["package"]), 1)
81            package = report["package"][0]
82            self.assertEqual(package["name"], "m4-native")
83            found_cves = { issue["id"]: issue["status"] for issue in package["issue"]}
84            self.assertIn("CVE-2008-1687", found_cves)
85            self.assertEqual(found_cves["CVE-2008-1687"], "Patched")
86
87        self.assertExists(summary_json)
88        check_m4_json(summary_json)
89        self.assertExists(recipe_json)
90        check_m4_json(recipe_json)
91
92
93    def test_image_json(self):
94        config = """
95INHERIT += "cve-check"
96CVE_CHECK_FORMAT_JSON = "1"
97"""
98        self.write_config(config)
99
100        vars = get_bb_vars(["CVE_CHECK_DIR", "CVE_CHECK_SUMMARY_DIR", "CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
101        report_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], vars["CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
102        print(report_json)
103        try:
104            os.remove(report_json)
105        except FileNotFoundError:
106            pass
107
108        bitbake("core-image-minimal-initramfs")
109        self.assertExists(report_json)
110
111        # Check that the summary report lists at least one package
112        with open(report_json) as f:
113            report = json.load(f)
114        self.assertEqual(report["version"], "1")
115        self.assertGreater(len(report["package"]), 1)
116
117        # Check that a random recipe wrote a recipe report to deploy/cve/
118        recipename = report["package"][0]["name"]
119        recipe_report = os.path.join(vars["CVE_CHECK_DIR"], recipename + "_cve.json")
120        self.assertExists(recipe_report)
121        with open(recipe_report) as f:
122            report = json.load(f)
123        self.assertEqual(report["version"], "1")
124        self.assertEqual(len(report["package"]), 1)
125        self.assertEqual(report["package"][0]["name"], recipename)
126
127
128    def test_recipe_report_json_unpatched(self):
129        config = """
130INHERIT += "cve-check"
131CVE_CHECK_FORMAT_JSON = "1"
132CVE_CHECK_REPORT_PATCHED = "0"
133"""
134        self.write_config(config)
135
136        vars = get_bb_vars(["CVE_CHECK_SUMMARY_DIR", "CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
137        summary_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], vars["CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
138        recipe_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], "m4-native_cve.json")
139
140        try:
141            os.remove(summary_json)
142            os.remove(recipe_json)
143        except FileNotFoundError:
144            pass
145
146        bitbake("m4-native -c cve_check")
147
148        def check_m4_json(filename):
149            with open(filename) as f:
150                report = json.load(f)
151            self.assertEqual(report["version"], "1")
152            self.assertEqual(len(report["package"]), 1)
153            package = report["package"][0]
154            self.assertEqual(package["name"], "m4-native")
155            #m4 had only Patched CVEs, so the issues array will be empty
156            self.assertEqual(package["issue"], [])
157
158        self.assertExists(summary_json)
159        check_m4_json(summary_json)
160        self.assertExists(recipe_json)
161        check_m4_json(recipe_json)
162
163
164    def test_recipe_report_json_ignored(self):
165        config = """
166INHERIT += "cve-check"
167CVE_CHECK_FORMAT_JSON = "1"
168CVE_CHECK_REPORT_PATCHED = "1"
169"""
170        self.write_config(config)
171
172        vars = get_bb_vars(["CVE_CHECK_SUMMARY_DIR", "CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
173        summary_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], vars["CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
174        recipe_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], "logrotate_cve.json")
175
176        try:
177            os.remove(summary_json)
178            os.remove(recipe_json)
179        except FileNotFoundError:
180            pass
181
182        bitbake("logrotate -c cve_check")
183
184        def check_m4_json(filename):
185            with open(filename) as f:
186                report = json.load(f)
187            self.assertEqual(report["version"], "1")
188            self.assertEqual(len(report["package"]), 1)
189            package = report["package"][0]
190            self.assertEqual(package["name"], "logrotate")
191            found_cves = { issue["id"]: issue["status"] for issue in package["issue"]}
192            # m4 CVE should not be in logrotate
193            self.assertNotIn("CVE-2008-1687", found_cves)
194            # logrotate has both Patched and Ignored CVEs
195            self.assertIn("CVE-2011-1098", found_cves)
196            self.assertEqual(found_cves["CVE-2011-1098"], "Patched")
197            self.assertIn("CVE-2011-1548", found_cves)
198            self.assertEqual(found_cves["CVE-2011-1548"], "Ignored")
199            self.assertIn("CVE-2011-1549", found_cves)
200            self.assertEqual(found_cves["CVE-2011-1549"], "Ignored")
201            self.assertIn("CVE-2011-1550", found_cves)
202            self.assertEqual(found_cves["CVE-2011-1550"], "Ignored")
203
204        self.assertExists(summary_json)
205        check_m4_json(summary_json)
206        self.assertExists(recipe_json)
207        check_m4_json(recipe_json)
208