def test_categorize_versions_without_affected_ranges():
    all_versions = {"1.0", "1.1", "2.0", "2.1", "3.0", "3.1"}

    unaffected_ranges = [
        VersionSpecifier.from_scheme_version_spec_string("semver", "< 1.2")
    ]
    affected_ranges = []
    resolved_ranges = [
        VersionSpecifier.from_scheme_version_spec_string("semver", ">= 3.0")
    ]

    unaffected_versions, affected_versions = categorize_versions(
        all_versions,
        unaffected_ranges,
        affected_ranges,
        resolved_ranges,
    )

    assert len(unaffected_versions) == 4
    assert "1.0" in unaffected_versions
    assert "1.1" in unaffected_versions
    assert "3.0" in unaffected_versions
    assert "3.1" in unaffected_versions

    assert len(affected_versions) == 2
    assert "2.0" in affected_versions
    assert "2.1" in affected_versions
def to_version_ranges(version_range_text):
    version_ranges = []
    range_expressions = version_range_text.split(",")
    for range_expression in range_expressions:
        if "to" in range_expression:
            # eg range_expression == "3.2.0 to 3.2.1"
            lower_bound, upper_bound = range_expression.split("to")
            lower_bound = f">={lower_bound}"
            upper_bound = f"<={upper_bound}"
            version_ranges.append(
                VersionSpecifier.from_scheme_version_spec_string(
                    "maven", f"{lower_bound},{upper_bound}"))

        elif "and later" in range_expression:
            # eg range_expression == "2.1.1 and later"
            range_expression = range_expression.replace("and later", "")
            version_ranges.append(
                VersionSpecifier.from_scheme_version_spec_string(
                    "maven", f">={range_expression}"))

        else:
            # eg  range_expression == "3.0.0"
            version_ranges.append(
                VersionSpecifier.from_scheme_version_spec_string(
                    "maven", range_expression))
    return version_ranges
def test_categorize_versions():
    flatbuffers_versions = MOCKED_CRATES_API_VERSIONS["flatbuffers"]

    unaffected_ranges = [
        VersionSpecifier.from_scheme_version_spec_string("semver", "< 0.4.0")
    ]
    affected_ranges = [
        VersionSpecifier.from_scheme_version_spec_string("semver", ">= 0.4.0"),
        VersionSpecifier.from_scheme_version_spec_string("semver", "<= 0.6.0"),
    ]
    resolved_ranges = [
        VersionSpecifier.from_scheme_version_spec_string("semver", ">= 0.6.1")
    ]

    unaffected_versions, affected_versions = categorize_versions(
        set(flatbuffers_versions),
        unaffected_ranges,
        affected_ranges,
        resolved_ranges,
    )

    assert len(unaffected_versions) == 2
    assert "0.3.0" in unaffected_versions
    assert "0.6.5" in unaffected_versions

    assert len(affected_versions) == 1
    assert "0.5.0" in affected_versions
Example #4
0
    def test_to_version_ranges(self):
        data = [
            {
                "version_affected": "?=",
                "version_value": "1.3.0",
            },
            {
                "version_affected": "=",
                "version_value": "1.3.1",
            },
            {
                "version_affected": "<",
                "version_value": "1.3.2",
            },
        ]
        fixed_version_ranges, affected_version_ranges = self.data_src.to_version_ranges(
            data)

        # Check fixed packages
        assert [
            VersionSpecifier.from_scheme_version_spec_string(
                "maven", ">=1.3.2")
        ] == fixed_version_ranges

        # Check vulnerable packages
        assert [
            VersionSpecifier.from_scheme_version_spec_string(
                "maven", "==1.3.0"),
            VersionSpecifier.from_scheme_version_spec_string(
                "maven", "==1.3.1"),
        ] == affected_version_ranges
Example #5
0
def categorize_versions(
    package_name: str,
    all_versions: Set[str],
    version_specs: Iterable[str],
) -> Tuple[Set[PackageURL], Set[PackageURL]]:
    """
    :return: impacted, resolved purls
    """
    impacted_versions, impacted_purls = set(), []
    vurl_specs = []
    for version_spec in version_specs:
        vurl_specs.append(VersionSpecifier.from_scheme_version_spec_string("pypi", version_spec))

    for version in all_versions:
        try:
            version_object = PYPIVersion(version)
        except:
            continue

        if any([version_object in vurl_spec for vurl_spec in vurl_specs]):
            impacted_versions.add(version)
            impacted_purls.append(
                PackageURL(
                    name=package_name,
                    type="pypi",
                    version=version,
                )
            )

    resolved_purls = []
    for version in all_versions - impacted_versions:
        resolved_purls.append(PackageURL(name=package_name, type="pypi", version=version))
    return impacted_purls, resolved_purls
def parse_version_ranges(string):
    """
    This method yields VersionSpecifier objects obtained by
    parsing `string`.
    >>> list(parse_version_ranges("Affects: 9.0.0.M1 to 9.0.0.M9")) == [
    ...     VersionSpecifier.from_scheme_version_spec_string('maven','<=9.0.0.M9,>=9.0.0.M1')
    ...  ]
    True
    >>> list(parse_version_ranges("Affects: 9.0.0.M1")) == [
    ...     VersionSpecifier.from_scheme_version_spec_string('maven','>=9.0.0.M1,<=9.0.0.M1')
    ...  ]
    True
    >>> list(parse_version_ranges("Affects: 9.0.0.M1 to 9.0.0.M9, 1.2.3 to 3.4.5")) == [
    ...     VersionSpecifier.from_scheme_version_spec_string('maven','<=9.0.0.M9,>=9.0.0.M1'),
    ...     VersionSpecifier.from_scheme_version_spec_string('maven','<=3.4.5,>=1.2.3')
    ...  ]
    True
    """
    version_rng_txt = string.split("Affects:")[-1].strip()
    version_ranges = version_rng_txt.split(",")
    for version_range in version_ranges:
        if "to" in version_range:
            lower_bound, upper_bound = version_range.split("to")
        elif "-" in version_range and not any(
            [i.isalpha() for i in version_range]):
            lower_bound, upper_bound = version_range.split("-")
        else:
            lower_bound = upper_bound = version_range

        yield VersionSpecifier.from_scheme_version_spec_string(
            "maven", f">={lower_bound},<={upper_bound}")
Example #7
0
def categorize_versions(
    all_versions: Set[str],
    affected_version_range: str,
    fixed_version_range: str,
) -> Tuple[Set[str], Set[str]]:
    """
    Seperate list of affected versions and unaffected versions from all versions
    using the ranges specified.

    :return: impacted, resolved versions
    """
    if not all_versions:
        # NPM registry has no data regarding this package, we skip these
        return set(), set()

    aff_spec = []
    fix_spec = []

    if affected_version_range:
        aff_specs = normalize_ranges(affected_version_range)
        aff_spec = [
            VersionSpecifier.from_scheme_version_spec_string("semver", spec)
            for spec in aff_specs
            if len(spec) >= 3
        ]

    if fixed_version_range:
        fix_specs = normalize_ranges(fixed_version_range)
        fix_spec = [
            VersionSpecifier.from_scheme_version_spec_string("semver", spec)
            for spec in fix_specs
            if len(spec) >= 3
        ]
    aff_ver, fix_ver = set(), set()
    # Unaffected version is that version which is in the fixed_version_range
    # or which is absent in the affected_version_range
    for ver in all_versions:
        ver_obj = SemverVersion(ver)

        if not any([ver_obj in spec for spec in aff_spec]) or any(
            [ver_obj in spec for spec in fix_spec]
        ):
            fix_ver.add(ver)
        else:
            aff_ver.add(ver)

    return aff_ver, fix_ver
    def to_version_ranges(self, versions_data):
        fixed_version_ranges = []
        affected_version_ranges = []
        for version_data in versions_data:
            version_value = version_data["version_value"]
            range_expression = version_data["version_affected"]
            if range_expression == "<":
                fixed_version_ranges.append(
                    VersionSpecifier.from_scheme_version_spec_string(
                        "maven", ">={}".format(version_value)
                    )
                )
            elif range_expression == "=" or range_expression == "?=":
                affected_version_ranges.append(
                    VersionSpecifier.from_scheme_version_spec_string(
                        "maven", "{}".format(version_value)
                    )
                )

        return (fixed_version_ranges, affected_version_ranges)
Example #9
0
    def extract_vuln_pkgs(self, vuln_info):
        vuln_status, version_infos = vuln_info.split(": ")
        if "none" in version_infos:
            return {}

        version_ranges = []
        windows_only = False
        for version_info in version_infos.split(", "):
            if version_info == "all":
                # This is misleading since eventually some version get fixed.
                continue

            if "-" not in version_info:
                # These are discrete versions
                version_ranges.append(
                    VersionSpecifier.from_scheme_version_spec_string("semver", version_info[0])
                )
                continue

            windows_only = "nginx/Windows" in version_info
            version_info = version_info.replace("nginx/Windows", "")
            lower_bound, upper_bound = version_info.split("-")

            version_ranges.append(
                VersionSpecifier.from_scheme_version_spec_string(
                    "semver", f">={lower_bound},<={upper_bound}"
                )
            )

        valid_versions = find_valid_versions(self.version_api.get("nginx/nginx"), version_ranges)
        qualifiers = {}
        if windows_only:
            qualifiers["os"] = "windows"

        return [
            PackageURL(type="generic", name="nginx", version=version, qualifiers=qualifiers)
            for version in valid_versions
        ]
Example #10
0
    def categorize_versions(
            package_type: str, version_range: str,
            all_versions: Set[str]) -> Tuple[List[str], List[str]]:
        version_class = version_class_by_package_type[package_type]
        version_scheme = version_class.scheme
        version_range = VersionSpecifier.from_scheme_version_spec_string(
            version_scheme, version_range)
        affected_versions = []
        unaffected_versions = []
        for version in all_versions:
            if version_class(version) in version_range:
                affected_versions.append(version)
            else:
                unaffected_versions.append(version)

        return (affected_versions, unaffected_versions)
Example #11
0
    def get_pkg_versions_from_ranges(self, version_range_list):
        """Takes a list of version ranges(affected) of a package
        as parameter and returns a tuple of safe package versions and
        vulnerable package versions"""
        all_version = self.version_api.get("istio/istio")
        safe_pkg_versions = []
        vuln_pkg_versions = []
        version_ranges = [
            VersionSpecifier.from_scheme_version_spec_string("semver", r)
            for r in version_range_list
        ]
        for version in all_version:
            version_obj = SemverVersion(version)
            if any([version_obj in v for v in version_ranges]):
                vuln_pkg_versions.append(version)

        safe_pkg_versions = set(all_version) - set(vuln_pkg_versions)
        return safe_pkg_versions, vuln_pkg_versions
Example #12
0
    def get_versions_for_pkg_from_range_list(self, version_range_list, pkg_name):
        # Takes a list of version ranges(pathced and unaffected) of a package
        # as parameter and returns a tuple of safe package versions and
        # vulnerable package versions

        safe_pkg_versions = []
        vuln_pkg_versions = []
        all_version_list = self.pkg_manager_api.get(pkg_name)
        if not version_range_list:
            return [], all_version_list
        version_ranges = [
            VersionSpecifier.from_scheme_version_spec_string("semver", r)
            for r in version_range_list
        ]
        for version in all_version_list:
            version_obj = SemverVersion(version)
            if any([version_obj in v for v in version_ranges]):
                safe_pkg_versions.append(version)

        vuln_pkg_versions = set(all_version_list) - set(safe_pkg_versions)
        return safe_pkg_versions, vuln_pkg_versions
Example #13
0
    def categorize_versions(all_versions, unaffected_version_ranges):

        for id, elem in enumerate(unaffected_version_ranges):
            unaffected_version_ranges[
                id] = VersionSpecifier.from_scheme_version_spec_string(
                    "semver", elem)

        safe_versions = []
        vulnerable_versions = []
        for i in all_versions:
            vobj = SemverVersion(i)
            is_vulnerable = False
            for ver_rng in unaffected_version_ranges:
                if vobj in ver_rng:
                    safe_versions.append(i)
                    is_vulnerable = True
                    break

            if not is_vulnerable:
                vulnerable_versions.append(i)

        return safe_versions, vulnerable_versions
Example #14
0
    def extract_fixed_pkgs(self, vuln_info):
        vuln_status, version_info = vuln_info.split(": ")
        if "none" in version_info:
            return {}

        raw_ranges = version_info.split(",")
        version_ranges = []
        for rng in raw_ranges:
            # Eg. "1.7.3+" gets converted to VersionSpecifier.from_scheme_version_spec_string("semver","^1.7.3")
            # The advisory in this case uses `+` in the sense that any version
            # with greater or equal `minor` version satisfies the range.
            # "1.7.4" satisifes "1.7.3+", but "1.8.4" does not. "1.7.3+" has same
            # semantics as that of "^1.7.3"

            version_ranges.append(
                VersionSpecifier.from_scheme_version_spec_string("semver", "^" + rng[:-1])
            )

        valid_versions = find_valid_versions(self.version_api.get("nginx/nginx"), version_ranges)

        return [
            PackageURL(type="generic", name="nginx", version=version) for version in valid_versions
        ]
Example #15
0
def build_report(package_vulnerability_index, detected_packages):
    report = []
    _package_index = {}
    for index, package in enumerate(detected_packages):
        if package not in _package_index:
            report.append({
                "package_name": package.name,
                "package_version": package.version,
                "vulnerabilities": []
            })
            _package_index[package.name] = index
        else:
            index = _package_index[package.name]

        if package.name in package_vulnerability_index:
            version_object = PYPIVersion(package.version)
            for vulnerability_data in package_vulnerability_index[
                    package.name]:
                if version_object in VersionSpecifier.from_scheme_version_spec_string(
                        "pypi", vulnerability_data["version_range"]):
                    report[index]["vulnerabilities"].extend(
                        vulnerability_data["vulnerabilities"])

    return json.dumps(report, indent=4)
Example #16
0
    def test_to_version_ranges(self):
        # Check single version
        assert [
            VersionSpecifier.from_scheme_version_spec_string("maven", "=3.2.2")
        ] == to_version_ranges("3.2.2")

        # Check range with lower and upper bounds
        assert [
            VersionSpecifier.from_scheme_version_spec_string("maven", ">=3.2.2, <=3.2.3")
        ] == to_version_ranges("3.2.2 to 3.2.3")

        # Check range with "and later"
        assert [
            VersionSpecifier.from_scheme_version_spec_string("maven", ">=3.2.2")
        ] == to_version_ranges("3.2.2 and later")

        # Check combination of above cases
        assert [
            VersionSpecifier.from_scheme_version_spec_string("maven", ">=3.2.2"),
            VersionSpecifier.from_scheme_version_spec_string("maven", ">=3.2.2, <=3.2.3"),
            VersionSpecifier.from_scheme_version_spec_string("maven", "==3.2.2"),
        ] == to_version_ranges("3.2.2 and later, 3.2.2 to 3.2.3, 3.2.2")
    def get_data_from_xml_doc(self, xml_doc: ET.ElementTree, pkg_metadata={}) -> List[Advisory]:
        """
        The orchestration method of the OvalDataSource. This method breaks an
        OVAL xml ElementTree into a list of `Advisory`.

        Note: pkg_metadata is a mapping of Package URL data that MUST INCLUDE
        "type" key.

        Example value of pkg_metadata:
                {"type":"deb","qualifiers":{"distro":"buster"} }
        """

        all_adv = []
        oval_doc = OvalParser(self.translations, xml_doc)
        raw_data = oval_doc.get_data()
        all_pkgs = self._collect_pkgs(raw_data)
        self.set_api(all_pkgs)

        # convert definition_data to Advisory objects
        for definition_data in raw_data:
            # These fields are definition level, i.e common for all elements
            # connected/linked to an OvalDefinition
            vuln_id = definition_data["vuln_id"]
            description = definition_data["description"]
            references = [Reference(url=url) for url in definition_data["reference_urls"]]
            affected_packages = []
            for test_data in definition_data["test_data"]:
                for package_name in test_data["package_list"]:
                    if package_name and len(package_name) >= 50:
                        continue

                    affected_version_range = test_data["version_ranges"] or set()
                    version_class = version_class_by_package_type[pkg_metadata["type"]]
                    version_scheme = version_class.scheme

                    affected_version_range = VersionSpecifier.from_scheme_version_spec_string(
                        version_scheme, affected_version_range
                    )
                    all_versions = self.pkg_manager_api.get(package_name)

                    # FIXME: what is this 50 DB limit? that's too small for versions
                    # FIXME: we should not drop data this way
                    # This filter is for filtering out long versions.
                    # 50 is limit because that's what db permits atm.
                    all_versions = [version for version in all_versions if len(version) < 50]
                    if not all_versions:
                        continue

                    affected_purls = []
                    safe_purls = []
                    for version in all_versions:
                        purl = self.create_purl(
                            pkg_name=package_name,
                            pkg_version=version,
                            pkg_data=pkg_metadata,
                        )
                        if version_class(version) in affected_version_range:
                            affected_purls.append(purl)
                        else:
                            safe_purls.append(purl)

                    affected_packages.extend(
                        nearest_patched_package(affected_purls, safe_purls),
                    )

            all_adv.append(
                Advisory(
                    summary=description,
                    affected_packages=affected_packages,
                    vulnerability_id=vuln_id,
                    references=references,
                )
            )
        return all_adv
Example #18
0
    def _load_advisory(self, path: str) -> Optional[Advisory]:
        record = get_advisory_data(path)
        advisory = record.get("advisory", {})
        crate_name = advisory["package"]
        references = []
        if advisory.get("url"):
            references.append(Reference(url=advisory["url"]))

        all_versions = self.crates_api.get(crate_name)

        # FIXME: Avoid wildcard version ranges for now.
        # See https://github.com/RustSec/advisory-db/discussions/831
        affected_ranges = [
            VersionSpecifier.from_scheme_version_spec_string("semver", r)
            for r in chain.from_iterable(
                record.get("affected", {}).get("functions", {}).values())
            if r != "*"
        ]

        unaffected_ranges = [
            VersionSpecifier.from_scheme_version_spec_string("semver", r)
            for r in record.get("versions", {}).get("unaffected", [])
            if r != "*"
        ]
        resolved_ranges = [
            VersionSpecifier.from_scheme_version_spec_string("semver", r)
            for r in record.get("versions", {}).get("patched", []) if r != "*"
        ]

        unaffected, affected = categorize_versions(all_versions,
                                                   unaffected_ranges,
                                                   affected_ranges,
                                                   resolved_ranges)

        impacted_purls = [
            PackageURL(type="cargo", name=crate_name, version=v)
            for v in affected
        ]
        resolved_purls = [
            PackageURL(type="cargo", name=crate_name, version=v)
            for v in unaffected
        ]

        cve_id = None
        if "aliases" in advisory:
            for alias in advisory["aliases"]:
                if alias.startswith("CVE-"):
                    cve_id = alias
                    break

        references.append(
            Reference(
                reference_id=advisory["id"],
                url="https://rustsec.org/advisories/{}.html".format(
                    advisory["id"]),
            ))

        return Advisory(
            summary=advisory.get("description", ""),
            affected_packages=nearest_patched_package(impacted_purls,
                                                      resolved_purls),
            vulnerability_id=cve_id,
            references=references,
        )