Example #1
0
    def test_make_request(self, ClientSession: AsyncMock):
        request = ClientSession.return_value.request
        fake_response = request.return_value.__aenter__.return_value
        fake_response.json.return_value = {"result": "fake"}
        api = AsyncErrataAPI("https://errata.example.com")

        actual = get_event_loop().run_until_complete(
            api._make_request("HEAD", "/api/path"))
        request.assert_called_once_with("HEAD",
                                        "https://errata.example.com/api/path",
                                        headers=api._headers)
        fake_response.raise_for_status.assert_called_once_with()
        fake_response.json.assert_awaited_once_with()
        self.assertEqual(actual, {"result": "fake"})

        headers = {"X-Test-Header": "Test Value"}
        request.reset_mock()
        fake_response.read.return_value = b"daedbeef"
        fake_response.raise_for_status.reset_mock()
        actual = get_event_loop().run_until_complete(
            api._make_request("GET",
                              "/api/path",
                              headers=headers,
                              parse_json=False))
        request.assert_called_once_with("GET",
                                        "https://errata.example.com/api/path",
                                        headers=headers)
        fake_response.raise_for_status.assert_called_once_with()
        fake_response.read.assert_awaited_once_with()
        self.assertEqual(actual, b"daedbeef")
Example #2
0
 def test_login(self, SecurityContext: Mock, ClientSession: Mock):
     client_ctx = SecurityContext.return_value
     client_ctx.step.return_value = b"faketoken"
     api = AsyncErrataAPI("https://errata.example.com")
     get_event_loop().run_until_complete(api.login())
     client_ctx.step.assert_called_once_with(b"")
     self.assertEqual(
         api._headers["Authorization"],
         'Negotiate ' + base64.b64encode(b"faketoken").decode())
Example #3
0
 def test_get_cves(self, _make_request: Mock, ClientSession: Mock):
     api = AsyncErrataAPI("https://errata.example.com")
     api.get_advisory = AsyncMock(
         return_value={"content": {
             "content": {
                 "cve": "A B C"
             }
         }})
     actual = get_event_loop().run_until_complete(api.get_cves(1))
     api.get_advisory.assert_awaited_once_with(1)
     self.assertEqual(actual, ["A", "B", "C"])
Example #4
0
 def test_create_delete_cve_package_exclusion(self, _make_request: Mock,
                                              ClientSession: Mock):
     api = AsyncErrataAPI("https://errata.example.com")
     _make_request.return_value = b""
     actual = get_event_loop().run_until_complete(
         api.delete_cve_package_exclusion(100))
     _make_request.assert_awaited_once_with(
         ANY,
         'DELETE',
         '/api/v1/cve_package_exclusion/100',
         parse_json=False)
     self.assertEqual(actual, None)
Example #5
0
    def test_get_advisory(self, _make_request: Mock, ClientSession: Mock):
        api = AsyncErrataAPI("https://errata.example.com")
        _make_request.return_value = {"result": "fake"}

        actual = get_event_loop().run_until_complete(api.get_advisory(1))
        _make_request.assert_awaited_once_with(ANY, "GET", "/api/v1/erratum/1")
        self.assertEqual(actual, {"result": "fake"})

        _make_request.reset_mock()
        actual = get_event_loop().run_until_complete(
            api.get_advisory("RHBA-2021:0001"))
        _make_request.assert_awaited_once_with(
            ANY, "GET", "/api/v1/erratum/RHBA-2021%3A0001")
        self.assertEqual(actual, {"result": "fake"})
Example #6
0
 def test_create_cve_package_exclusion(self, _make_request: Mock,
                                       ClientSession: Mock):
     api = AsyncErrataAPI("https://errata.example.com")
     _make_request.return_value = {"result": "fake"}
     actual = get_event_loop().run_until_complete(
         api.create_cve_package_exclusion(1, "CVE-1", "a"))
     _make_request.assert_awaited_once_with(ANY,
                                            'POST',
                                            '/api/v1/cve_package_exclusion',
                                            json={
                                                'cve': 'CVE-1',
                                                'errata': 1,
                                                'package': 'a'
                                            })
     self.assertEqual(actual, {"result": "fake"})
 def __init__(self,
              runtime: Runtime,
              use_jira: bool = False,
              output: str = 'text'):
     self.runtime = runtime
     self.use_jira = use_jira
     bug_tracker_cls = JIRABugTracker if use_jira else BugzillaBugTracker
     self.config = bug_tracker_cls.get_config(runtime)
     self.bug_tracker = bug_tracker_cls(self.config)
     self.target_releases: List[str] = self.config['target_release']
     self.product: str = self.config['product']
     self.et_data: Dict[str, Any] = runtime.gitdata.load_data(
         key='erratatool').data
     self.errata_api = AsyncErrataAPI(
         self.et_data.get("server", constants.errata_url))
     self.problems: List[str] = []
     self.output = output
Example #8
0
 def test_get_builds(self, _make_request: Mock, ClientSession: Mock):
     api = AsyncErrataAPI("https://errata.example.com")
     _make_request.return_value = {
         "ProductVersion1": {
             "builds": [{
                 "a-1.0.0-1": {},
                 "b-1.0.0-1": {}
             }]
         },
         "ProductVersion2": {
             "builds": [{
                 "c-1.0.0-1": {}
             }]
         }
     }
     actual = get_event_loop().run_until_complete(api.get_builds(1))
     _make_request.assert_awaited_once_with(
         ANY, "GET", "/api/v1/erratum/1/builds_list")
     self.assertEqual(actual, _make_request.return_value)
Example #9
0
async def get_flaws(runtime, advisory, bug_tracker, flaw_bug_tracker, noop):
    # get attached bugs from advisory
    advisory_bug_ids = bug_tracker.advisory_bug_ids(advisory)
    if not advisory_bug_ids:
        runtime.logger.info(f'Found 0 {bug_tracker.type} bugs attached')
        return []

    attached_tracker_bugs: List[Bug] = bug_tracker.get_tracker_bugs(advisory_bug_ids, verbose=runtime.debug)
    runtime.logger.info(f'Found {len(attached_tracker_bugs)} {bug_tracker.type} tracker bugs attached: '
                        f'{sorted([b.id for b in attached_tracker_bugs])}')
    if not attached_tracker_bugs:
        return []

    # validate and get target_release
    current_target_release = Bug.get_target_release(attached_tracker_bugs)
    tracker_flaws, flaw_id_bugs = bug_tracker.get_corresponding_flaw_bugs(
        attached_tracker_bugs,
        flaw_bug_tracker,
        strict=True
    )
    runtime.logger.info(f'Found {len(flaw_id_bugs)} {flaw_bug_tracker.type} corresponding flaw bugs:'
                        f' {sorted(flaw_id_bugs.keys())}')

    # current_target_release is digit.digit.[z|0]
    # if current_target_release is GA then run first-fix bug filtering
    # for GA not every flaw bug is considered first-fix
    # for z-stream every flaw bug is considered first-fix
    if current_target_release[-1] == 'z':
        runtime.logger.info("Detected z-stream target release, every flaw bug is considered first-fix")
        first_fix_flaw_bugs = list(flaw_id_bugs.values())
    else:
        runtime.logger.info("Detected GA release, applying first-fix filtering..")
        first_fix_flaw_bugs = [
            flaw_bug for flaw_bug in flaw_id_bugs.values()
            if is_first_fix_any(flaw_bug_tracker, flaw_bug, current_target_release)
        ]

    runtime.logger.info(f'{len(first_fix_flaw_bugs)} out of {len(flaw_id_bugs)} flaw bugs considered "first-fix"')
    if not first_fix_flaw_bugs:
        return []

    runtime.logger.info('Associating CVEs with builds')
    errata_config = runtime.gitdata.load_data(key='erratatool').data
    errata_api = AsyncErrataAPI(errata_config.get("server", constants.errata_url))
    try:
        await errata_api.login()
        await associate_builds_with_cves(errata_api, advisory, attached_tracker_bugs, tracker_flaws, flaw_id_bugs, noop)
    except ValueError as e:
        runtime.logger.warn(f"Error associating builds with cves: {e}")
    finally:
        await errata_api.close()
    return first_fix_flaw_bugs
Example #10
0
    def test_get_cve_package_exclusions(self, _make_request: Mock,
                                        ClientSession: Mock):
        api = AsyncErrataAPI("https://errata.example.com")
        _make_request.side_effect = lambda _0, _1, _2, params: {
            1: {
                "data": [{
                    "id": 1
                }, {
                    "id": 2
                }, {
                    "id": 3
                }]
            },
            2: {
                "data": [{
                    "id": 4
                }, {
                    "id": 5
                }]
            },
            3: {
                "data": []
            }
        }[params['page[number]']]

        async def _call():
            items = []
            async for item in api.get_cve_package_exclusions(1):
                items.append(item)
            return items

        actual = get_event_loop().run_until_complete(_call())
        _make_request.assert_awaited_with(ANY,
                                          'GET',
                                          '/api/v1/cve_package_exclusion',
                                          params={
                                              'filter[errata_id]': '1',
                                              'page[number]': 3,
                                              'page[size]': 300
                                          })
        self.assertEqual(actual, [{
            'id': 1
        }, {
            'id': 2
        }, {
            'id': 3
        }, {
            'id': 4
        }, {
            'id': 5
        }])
Example #11
0
 def test_close(self, ClientSession: Mock):
     api = AsyncErrataAPI("https://errata.example.com")
     get_event_loop().run_until_complete(api.close())
     ClientSession.return_value.close.assert_called_once()
class BugValidator:
    def __init__(self,
                 runtime: Runtime,
                 use_jira: bool = False,
                 output: str = 'text'):
        self.runtime = runtime
        self.use_jira = use_jira
        bug_tracker_cls = JIRABugTracker if use_jira else BugzillaBugTracker
        self.config = bug_tracker_cls.get_config(runtime)
        self.bug_tracker = bug_tracker_cls(self.config)
        self.target_releases: List[str] = self.config['target_release']
        self.product: str = self.config['product']
        self.et_data: Dict[str, Any] = runtime.gitdata.load_data(
            key='erratatool').data
        self.errata_api = AsyncErrataAPI(
            self.et_data.get("server", constants.errata_url))
        self.problems: List[str] = []
        self.output = output

    async def close(self):
        await self.errata_api.close()

    def validate(
        self,
        non_flaw_bugs: Iterable[Bug],
        verify_bug_status: bool,
    ):
        non_flaw_bugs = self.filter_bugs_by_release(non_flaw_bugs,
                                                    complain=True)
        blocking_bugs_for = self._get_blocking_bugs_for(non_flaw_bugs)
        self._verify_blocking_bugs(blocking_bugs_for)

        if verify_bug_status:
            self._verify_bug_status(non_flaw_bugs)

        if self.problems:
            if self.output != 'slack':
                red_print(
                    "Some bug problems were listed above. Please investigate.")
            exit(1)
        green_print(
            "All bugs were verified. This check doesn't cover CVE flaw bugs.")

    async def verify_attached_flaws(self, advisory_bugs: Dict[int, List[Bug]]):
        futures = []
        for advisory_id, attached_bugs in advisory_bugs.items():
            attached_trackers = [
                b for b in attached_bugs if b.is_tracker_bug()
            ]
            attached_flaws = [b for b in attached_bugs if b.is_flaw_bug()]
            futures.append(
                self._verify_attached_flaws_for(advisory_id, attached_trackers,
                                                attached_flaws))
        await asyncio.gather(*futures)
        if self.problems:
            red_print(
                "Some bug problems were listed above. Please investigate.")
            exit(1)
        green_print("All CVE flaw bugs were verified.")

    async def _verify_attached_flaws_for(self, advisory_id: int,
                                         attached_trackers: Iterable[Bug],
                                         attached_flaws: Iterable[Bug]):
        # Retrieve flaw bugs in Bugzilla for attached_tracker_bugs
        tracker_flaws, flaw_id_bugs = self.bug_tracker.get_corresponding_flaw_bugs(
            attached_trackers)

        # Find first-fix flaws
        first_fix_flaw_ids = set()
        if attached_trackers:
            current_target_release = bzutil.Bug.get_target_release(
                attached_trackers)
            if current_target_release[-1] == 'z':
                first_fix_flaw_ids = flaw_id_bugs.keys()
            else:
                first_fix_flaw_ids = {
                    flaw_bug.id
                    for flaw_bug in flaw_id_bugs.values()
                    if bzutil.is_first_fix_any(self.bug_tracker.client(
                    ), flaw_bug, current_target_release)
                }

        # Check if attached flaws match attached trackers
        attached_flaw_ids = {b.id for b in attached_flaws}
        missing_flaw_ids = attached_flaw_ids - first_fix_flaw_ids
        if missing_flaw_ids:
            self._complain(
                f"On advisory {advisory_id}, {len(missing_flaw_ids)} flaw bugs are not attached but they are referenced by attached tracker bugs: {', '.join(sorted(map(str, missing_flaw_ids)))}."
                " You probably need to attach those flaw bugs or drop the corresponding tracker bugs."
            )
        extra_flaw_ids = first_fix_flaw_ids - attached_flaw_ids
        if extra_flaw_ids:
            self._complain(
                f"On advisory {advisory_id}, {len(extra_flaw_ids)} flaw bugs are attached but there are no tracker bugs referencing them: {', '.join(sorted(map(str, extra_flaw_ids)))}."
                " You probably need to drop those flaw bugs or attach the corresponding tracker bugs."
            )

        # Check if advisory is of the expected type
        advisory_info = await self.errata_api.get_advisory(advisory_id)
        advisory_type = next(iter(advisory_info["errata"].keys())).upper(
        )  # should be one of [RHBA, RHSA, RHEA]
        if not first_fix_flaw_ids:
            if advisory_type == "RHSA":
                self._complain(
                    f"Advisory {advisory_id} is of type {advisory_type} but has no first-fix flaw bugs. It should be converted to RHBA or RHEA."
                )
            return  # The remaining checks are not needed for a non-RHSA.
        if advisory_type != "RHSA":
            self._complain(
                f"Advisory {advisory_id} is of type {advisory_type} but has first-fix flaw bugs {first_fix_flaw_ids}. It should be converted to RHSA."
            )

        # Check if flaw bugs are associated with specific builds
        cve_components_mapping: Dict[str, Set[str]] = {}
        for tracker in attached_trackers:
            component_name = tracker.whiteboard_component
            if not component_name:
                raise ValueError(
                    f"Tracker bug {tracker.id} doesn't have a valid component name in its whiteboard field."
                )
            flaw_ids = tracker_flaws[tracker.id]
            for flaw_id in flaw_ids:
                if len(flaw_id_bugs[flaw_id].alias) != 1:
                    raise ValueError(
                        f"Flaw bug {flaw_id} should have exact 1 alias.")
                cve = flaw_id_bugs[flaw_id].alias[0]
                cve_components_mapping.setdefault(cve,
                                                  set()).add(component_name)
        current_cve_package_exclusions = await AsyncErrataUtils.get_advisory_cve_package_exclusions(
            self.errata_api, advisory_id)
        attached_builds = await self.errata_api.get_builds_flattened(
            advisory_id)
        expected_cve_packages_exclusions = AsyncErrataUtils.compute_cve_package_exclusions(
            attached_builds, cve_components_mapping)
        extra_cve_package_exclusions, missing_cve_package_exclusions = AsyncErrataUtils.diff_cve_package_exclusions(
            current_cve_package_exclusions, expected_cve_packages_exclusions)
        for cve, cve_package_exclusions in extra_cve_package_exclusions.items(
        ):
            if cve_package_exclusions:
                self._complain(
                    f"On advisory {advisory_id}, {cve} is not associated with Brew components {', '.join(sorted(cve_package_exclusions))}."
                    " You may need to associate the CVE with the components in the CVE mapping or drop the tracker bugs."
                )
        for cve, cve_package_exclusions in missing_cve_package_exclusions.items(
        ):
            if cve_package_exclusions:
                self._complain(
                    f"On advisory {advisory_id}, {cve} is associated with Brew components {', '.join(sorted(cve_package_exclusions))} without a tracker bug."
                    " You may need to explictly exclude those Brew components from the CVE mapping or attach the corresponding tracker bugs."
                )

        # Check if flaw bugs match the CVE field of the advisory
        advisory_cves = advisory_info["content"]["content"]["cve"].split()
        extra_cves = cve_components_mapping.keys() - advisory_cves
        if extra_cves:
            self._complain(
                f"On advisory {advisory_id}, bugs for the following CVEs are already attached but they are not listed in advisory's `CVE Names` field: {', '.join(sorted(extra_cves))}"
            )
        missing_cves = advisory_cves - cve_components_mapping.keys()
        if missing_cves:
            self._complain(
                f"On advisory {advisory_id}, bugs for the following CVEs are not attached but listed in advisory's `CVE Names` field: {', '.join(sorted(missing_cves))}"
            )

    async def get_attached_bugs(
            self, advisory_ids: Iterable[str]) -> Dict[int, Set[Bug]]:
        """ Get bugs attached to specified advisories
        :return: a dict with advisory id as key and set of bug objects as value
        """
        green_print(f"Retrieving bugs for advisory {advisory_ids}")
        if self.use_jira:
            issue_keys = {
                advisory_id: [
                    issue["key"] for issue in
                    errata.get_jira_issue_from_advisory(advisory_id)
                ]
                for advisory_id in advisory_ids
            }
            bug_map = self.bug_tracker.get_bugs_map(
                [key for keys in issue_keys.values() for key in keys])
            result = {
                advisory_id: {bug_map[key]
                              for key in issue_keys[advisory_id]}
                for advisory_id in advisory_ids
            }
        else:
            advisories = await asyncio.gather(*[
                self.errata_api.get_advisory(advisory_id)
                for advisory_id in advisory_ids
            ])
            bug_map = self.bug_tracker.get_bugs_map(
                list({
                    b["bug"]["id"]
                    for ad in advisories for b in ad["bugs"]["bugs"]
                }))
            result = {
                ad["content"]["content"]["errata_id"]:
                {bug_map[b["bug"]["id"]]
                 for b in ad["bugs"]["bugs"]}
                for ad in advisories
            }
        return result

    def filter_bugs_by_product(self, bugs):
        # filter out bugs for different product (presumably security flaw bugs)
        return [b for b in bugs if b.product == self.product]

    def filter_bugs_by_release(self,
                               bugs: Iterable[Bug],
                               complain: bool = False) -> List[Bug]:
        # filter out bugs with an invalid target release
        filtered_bugs = []
        for b in bugs:
            # b.target release is a list of size 0 or 1
            if any(target in self.target_releases
                   for target in b.target_release):
                filtered_bugs.append(b)
            elif complain:
                self._complain(
                    f"bug {b.id} target release {b.target_release} is not in "
                    f"{self.target_releases}. Does it belong in this release?")
        return filtered_bugs

    def _get_blocking_bugs_for(self, bugs):
        # get blocker bugs in the next version for all bugs we are examining
        candidate_blockers = [b.depends_on for b in bugs if b.depends_on]
        candidate_blockers = {b for deps in candidate_blockers for b in deps}

        v = minor_version_tuple(self.target_releases[0])
        next_version = (v[0], v[1] + 1)

        pattern = re.compile(r'^[0-9]+\.[0-9]+\.(0|z)$')

        # retrieve blockers and filter to those with correct product and target version
        blocking_bugs = {
            bug.id: bug
            for bug in self.bug_tracker.get_bugs(list(candidate_blockers))
            # b.target release is a list of size 0 or 1
            if any(
                minor_version_tuple(target) == next_version
                for target in bug.target_release
                if pattern.match(target)) and bug.product == self.product
        }

        return {
            bug:
            [blocking_bugs[b] for b in bug.depends_on if b in blocking_bugs]
            for bug in bugs
        }

    def _verify_blocking_bugs(self, blocking_bugs_for):
        # complain about blocker bugs that aren't verified or shipped
        for bug, blockers in blocking_bugs_for.items():
            for blocker in blockers:
                message = str()
                if blocker.status not in [
                        'VERIFIED', 'RELEASE_PENDING', 'CLOSED'
                ]:
                    if self.output == 'text':
                        message = f"Regression possible: {bug.status} bug {bug.id} is a backport of bug " \
                            f"{blocker.id} which has status {blocker.status}"
                    elif self.output == 'slack':
                        message = f"`{bug.status}` bug <{bug.weburl}|{bug.id}> is a backport of " \
                                  f"`{blocker.status}` bug <{blocker.weburl}|{blocker.id}>"
                    self._complain(message)
                if blocker.status == 'CLOSED' and blocker.resolution not in [
                        'CURRENTRELEASE', 'NEXTRELEASE', 'ERRATA', 'DUPLICATE',
                        'NOTABUG', 'WONTFIX'
                ]:
                    if self.output == 'text':
                        message = f"Regression possible: {bug.status} bug {bug.id} is a backport of bug " \
                            f"{blocker.id} which was CLOSED {blocker.resolution}"
                    elif self.output == 'slack':
                        message = f"`{bug.status}` bug <{bug.weburl}|{bug.id}> is a backport of bug " \
                            f"<{blocker.weburl}|{blocker.id}> which was CLOSED `{blocker.resolution}`"
                    self._complain(message)

    def _verify_bug_status(self, bugs):
        # complain about bugs that are not yet VERIFIED or more.
        for bug in bugs:
            if bug.is_flaw_bug():
                continue
            if bug.status in ["VERIFIED", "RELEASE_PENDING"]:
                continue
            if bug.status == "CLOSED" and bug.resolution == "ERRATA":
                continue
            status = f"{bug.status}"
            if bug.status == 'CLOSED':
                status = f"{bug.status}: {bug.resolution}"
            self._complain(f"Bug {bug.id} has status {status}")

    def _complain(self, problem: str):
        red_print(problem)
        self.problems.append(problem)