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_convert_cve_version(self):
58        from oe.cve_check import convert_cve_version
59
60        # Default format
61        self.assertEqual(convert_cve_version("8.3"), "8.3")
62        self.assertEqual(convert_cve_version(""), "")
63
64        # OpenSSL format version
65        self.assertEqual(convert_cve_version("1.1.1t"), "1.1.1t")
66
67        # OpenSSH format
68        self.assertEqual(convert_cve_version("8.3_p1"), "8.3p1")
69        self.assertEqual(convert_cve_version("8.3_p22"), "8.3p22")
70
71        # Linux kernel format
72        self.assertEqual(convert_cve_version("6.2_rc8"), "6.2-rc8")
73        self.assertEqual(convert_cve_version("6.2_rc31"), "6.2-rc31")
74
75
76    def test_recipe_report_json(self):
77        config = """
78INHERIT += "cve-check"
79CVE_CHECK_FORMAT_JSON = "1"
80"""
81        self.write_config(config)
82
83        vars = get_bb_vars(["CVE_CHECK_SUMMARY_DIR", "CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
84        summary_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], vars["CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
85        recipe_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], "m4-native_cve.json")
86
87        try:
88            os.remove(summary_json)
89            os.remove(recipe_json)
90        except FileNotFoundError:
91            pass
92
93        bitbake("m4-native -c cve_check")
94
95        def check_m4_json(filename):
96            with open(filename) as f:
97                report = json.load(f)
98            self.assertEqual(report["version"], "1")
99            self.assertEqual(len(report["package"]), 1)
100            package = report["package"][0]
101            self.assertEqual(package["name"], "m4-native")
102            found_cves = { issue["id"]: issue["status"] for issue in package["issue"]}
103            self.assertIn("CVE-2008-1687", found_cves)
104            self.assertEqual(found_cves["CVE-2008-1687"], "Patched")
105
106        self.assertExists(summary_json)
107        check_m4_json(summary_json)
108        self.assertExists(recipe_json)
109        check_m4_json(recipe_json)
110
111
112    def test_image_json(self):
113        config = """
114INHERIT += "cve-check"
115CVE_CHECK_FORMAT_JSON = "1"
116"""
117        self.write_config(config)
118
119        vars = get_bb_vars(["CVE_CHECK_DIR", "CVE_CHECK_SUMMARY_DIR", "CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
120        report_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], vars["CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
121        print(report_json)
122        try:
123            os.remove(report_json)
124        except FileNotFoundError:
125            pass
126
127        bitbake("core-image-minimal-initramfs")
128        self.assertExists(report_json)
129
130        # Check that the summary report lists at least one package
131        with open(report_json) as f:
132            report = json.load(f)
133        self.assertEqual(report["version"], "1")
134        self.assertGreater(len(report["package"]), 1)
135
136        # Check that a random recipe wrote a recipe report to deploy/cve/
137        recipename = report["package"][0]["name"]
138        recipe_report = os.path.join(vars["CVE_CHECK_DIR"], recipename + "_cve.json")
139        self.assertExists(recipe_report)
140        with open(recipe_report) as f:
141            report = json.load(f)
142        self.assertEqual(report["version"], "1")
143        self.assertEqual(len(report["package"]), 1)
144        self.assertEqual(report["package"][0]["name"], recipename)
145
146
147    def test_recipe_report_json_unpatched(self):
148        config = """
149INHERIT += "cve-check"
150CVE_CHECK_FORMAT_JSON = "1"
151CVE_CHECK_REPORT_PATCHED = "0"
152"""
153        self.write_config(config)
154
155        vars = get_bb_vars(["CVE_CHECK_SUMMARY_DIR", "CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
156        summary_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], vars["CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
157        recipe_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], "m4-native_cve.json")
158
159        try:
160            os.remove(summary_json)
161            os.remove(recipe_json)
162        except FileNotFoundError:
163            pass
164
165        bitbake("m4-native -c cve_check")
166
167        def check_m4_json(filename):
168            with open(filename) as f:
169                report = json.load(f)
170            self.assertEqual(report["version"], "1")
171            self.assertEqual(len(report["package"]), 1)
172            package = report["package"][0]
173            self.assertEqual(package["name"], "m4-native")
174            #m4 had only Patched CVEs, so the issues array will be empty
175            self.assertEqual(package["issue"], [])
176
177        self.assertExists(summary_json)
178        check_m4_json(summary_json)
179        self.assertExists(recipe_json)
180        check_m4_json(recipe_json)
181
182
183    def test_recipe_report_json_ignored(self):
184        config = """
185INHERIT += "cve-check"
186CVE_CHECK_FORMAT_JSON = "1"
187CVE_CHECK_REPORT_PATCHED = "1"
188"""
189        self.write_config(config)
190
191        vars = get_bb_vars(["CVE_CHECK_SUMMARY_DIR", "CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
192        summary_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], vars["CVE_CHECK_SUMMARY_FILE_NAME_JSON"])
193        recipe_json = os.path.join(vars["CVE_CHECK_SUMMARY_DIR"], "logrotate_cve.json")
194
195        try:
196            os.remove(summary_json)
197            os.remove(recipe_json)
198        except FileNotFoundError:
199            pass
200
201        bitbake("logrotate -c cve_check")
202
203        def check_m4_json(filename):
204            with open(filename) as f:
205                report = json.load(f)
206            self.assertEqual(report["version"], "1")
207            self.assertEqual(len(report["package"]), 1)
208            package = report["package"][0]
209            self.assertEqual(package["name"], "logrotate")
210            found_cves = {}
211            for issue in package["issue"]:
212                found_cves[issue["id"]] = {
213                    "status" : issue["status"],
214                    "detail" : issue["detail"] if "detail" in issue else "",
215                    "description" : issue["description"] if "description" in issue else ""
216                }
217            # m4 CVE should not be in logrotate
218            self.assertNotIn("CVE-2008-1687", found_cves)
219            # logrotate has both Patched and Ignored CVEs
220            self.assertIn("CVE-2011-1098", found_cves)
221            self.assertEqual(found_cves["CVE-2011-1098"]["status"], "Patched")
222            self.assertEqual(len(found_cves["CVE-2011-1098"]["detail"]), 0)
223            self.assertEqual(len(found_cves["CVE-2011-1098"]["description"]), 0)
224            detail = "not-applicable-platform"
225            description = "CVE is debian, gentoo or SUSE specific on the way logrotate was installed/used"
226            self.assertIn("CVE-2011-1548", found_cves)
227            self.assertEqual(found_cves["CVE-2011-1548"]["status"], "Ignored")
228            self.assertEqual(found_cves["CVE-2011-1548"]["detail"], detail)
229            self.assertEqual(found_cves["CVE-2011-1548"]["description"], description)
230            self.assertIn("CVE-2011-1549", found_cves)
231            self.assertEqual(found_cves["CVE-2011-1549"]["status"], "Ignored")
232            self.assertEqual(found_cves["CVE-2011-1549"]["detail"], detail)
233            self.assertEqual(found_cves["CVE-2011-1549"]["description"], description)
234            self.assertIn("CVE-2011-1550", found_cves)
235            self.assertEqual(found_cves["CVE-2011-1550"]["status"], "Ignored")
236            self.assertEqual(found_cves["CVE-2011-1550"]["detail"], detail)
237            self.assertEqual(found_cves["CVE-2011-1550"]["description"], description)
238
239        self.assertExists(summary_json)
240        check_m4_json(summary_json)
241        self.assertExists(recipe_json)
242        check_m4_json(recipe_json)
243