|
9 | 9 |
|
10 | 10 | import logging |
11 | 11 | import traceback |
| 12 | +from datetime import datetime |
12 | 13 | from pathlib import Path |
13 | 14 | from typing import Iterable |
14 | 15 | from typing import List |
| 16 | +from typing import Mapping |
15 | 17 | from typing import Optional |
16 | 18 |
|
17 | 19 | import pytz |
18 | 20 | import saneyaml |
19 | 21 | from dateutil import parser as dateparser |
| 22 | +from django.db.models.query import QuerySet |
20 | 23 | from packageurl import PackageURL |
21 | 24 | from univers.version_range import RANGE_CLASS_BY_SCHEMES |
22 | 25 | from univers.version_range import VersionRange |
|
27 | 30 | from vulnerabilities.importer import AffectedPackage |
28 | 31 | from vulnerabilities.importer import GitImporter |
29 | 32 | from vulnerabilities.importer import Reference |
| 33 | +from vulnerabilities.importer import UnMergeablePackageError |
| 34 | +from vulnerabilities.improver import Improver |
| 35 | +from vulnerabilities.improver import Inference |
| 36 | +from vulnerabilities.models import Advisory |
| 37 | +from vulnerabilities.package_managers import VERSION_API_CLASSES_BY_PACKAGE_TYPE |
| 38 | +from vulnerabilities.package_managers import GoproxyVersionAPI |
| 39 | +from vulnerabilities.package_managers import VersionAPI |
| 40 | +from vulnerabilities.package_managers import get_api_package_name |
| 41 | +from vulnerabilities.utils import AffectedPackage as LegacyAffectedPackage |
30 | 42 | from vulnerabilities.utils import build_description |
| 43 | +from vulnerabilities.utils import get_affected_packages_by_patched_package |
| 44 | +from vulnerabilities.utils import get_cwe_id |
| 45 | +from vulnerabilities.utils import nearest_patched_package |
| 46 | +from vulnerabilities.utils import resolve_version_range |
31 | 47 |
|
32 | 48 | logger = logging.getLogger(__name__) |
33 | 49 |
|
34 | | - |
35 | 50 | PURL_TYPE_BY_GITLAB_SCHEME = { |
36 | 51 | # "conan": "conan", |
37 | 52 | "gem": "gem", |
|
44 | 59 | "pypi": "pypi", |
45 | 60 | } |
46 | 61 |
|
47 | | - |
48 | 62 | GITLAB_SCHEME_BY_PURL_TYPE = {v: k for k, v in PURL_TYPE_BY_GITLAB_SCHEME.items()} |
49 | 63 |
|
50 | 64 |
|
@@ -188,6 +202,10 @@ def parse_gitlab_advisory(file): |
188 | 202 | summary = build_description(gitlab_advisory.get("title"), gitlab_advisory.get("description")) |
189 | 203 | urls = gitlab_advisory.get("urls") |
190 | 204 | references = [Reference.from_url(u) for u in urls] |
| 205 | + |
| 206 | + cwe_ids = gitlab_advisory.get("cwe_ids") or [] |
| 207 | + cwe_list = list(map(get_cwe_id, cwe_ids)) |
| 208 | + |
191 | 209 | date_published = dateparser.parse(gitlab_advisory.get("pubdate")) |
192 | 210 | date_published = date_published.replace(tzinfo=pytz.UTC) |
193 | 211 | package_slug = gitlab_advisory.get("package_slug") |
@@ -253,4 +271,102 @@ def parse_gitlab_advisory(file): |
253 | 271 | references=references, |
254 | 272 | date_published=date_published, |
255 | 273 | affected_packages=affected_packages, |
| 274 | + weaknesses=cwe_list, |
256 | 275 | ) |
| 276 | + |
| 277 | + |
| 278 | +class GitLabBasicImprover(Improver): |
| 279 | + """ |
| 280 | + Get the nearest fixed_version and then resolve the version range with the help of all valid versions. |
| 281 | + Generate inference between all the affected packages and the fixed_version that fixes all those affected packages. |
| 282 | +
|
| 283 | + In case of gitlab advisory data we get a list of fixed_versions and a affected_version_range. |
| 284 | + Since we can not determine which package fixes which range. |
| 285 | + """ |
| 286 | + |
| 287 | + def __init__(self) -> None: |
| 288 | + self.versions_fetcher_by_purl: Mapping[str, VersionAPI] = {} |
| 289 | + |
| 290 | + @property |
| 291 | + def interesting_advisories(self) -> QuerySet: |
| 292 | + return Advisory.objects.filter(created_by=GitLabAPIImporter.qualified_name) |
| 293 | + |
| 294 | + def get_package_versions( |
| 295 | + self, package_url: PackageURL, until: Optional[datetime] = None |
| 296 | + ) -> List[str]: |
| 297 | + """ |
| 298 | + Return a list of `valid_versions` for the `package_url` |
| 299 | + """ |
| 300 | + api_name = get_api_package_name(purl=package_url) |
| 301 | + if not api_name: |
| 302 | + logger.error(f"Could not get versions for {package_url!r}") |
| 303 | + return [] |
| 304 | + versions_fetcher = self.versions_fetcher_by_purl.get(package_url) |
| 305 | + if not versions_fetcher: |
| 306 | + versions_fetcher: VersionAPI = VERSION_API_CLASSES_BY_PACKAGE_TYPE[package_url.type] |
| 307 | + self.versions_fetcher_by_purl[package_url] = versions_fetcher() |
| 308 | + |
| 309 | + versions_fetcher = self.versions_fetcher_by_purl[package_url] |
| 310 | + |
| 311 | + self.versions_fetcher_by_purl[package_url] = versions_fetcher |
| 312 | + return versions_fetcher.get_until(package_name=api_name, until=until).valid_versions |
| 313 | + |
| 314 | + def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]: |
| 315 | + """ |
| 316 | + Yield Inferences for the given advisory data |
| 317 | + """ |
| 318 | + if not advisory_data.affected_packages: |
| 319 | + return iter([]) |
| 320 | + try: |
| 321 | + purl, affected_version_ranges, _ = AffectedPackage.merge( |
| 322 | + advisory_data.affected_packages |
| 323 | + ) |
| 324 | + except UnMergeablePackageError: |
| 325 | + logger.error(f"Cannot merge with different purls {advisory_data.affected_packages!r}") |
| 326 | + return iter([]) |
| 327 | + |
| 328 | + pkg_type = purl.type |
| 329 | + pkg_namespace = purl.namespace |
| 330 | + pkg_name = purl.name |
| 331 | + if purl.type == "golang": |
| 332 | + # Problem with the Golang and Go that they provide full path |
| 333 | + # FIXME: We need to get the PURL subpath for Go module |
| 334 | + versions_fetcher = self.versions_fetcher_by_purl.get(purl) |
| 335 | + if not versions_fetcher: |
| 336 | + versions_fetcher = GoproxyVersionAPI() |
| 337 | + self.versions_fetcher_by_purl[purl] = versions_fetcher |
| 338 | + pkg_name = versions_fetcher.module_name_by_package_name.get(pkg_name, pkg_name) |
| 339 | + |
| 340 | + valid_versions = self.get_package_versions( |
| 341 | + package_url=purl, until=advisory_data.date_published |
| 342 | + ) |
| 343 | + for affected_version_range in affected_version_ranges: |
| 344 | + aff_vers, unaff_vers = resolve_version_range( |
| 345 | + affected_version_range=affected_version_range, |
| 346 | + package_versions=valid_versions, |
| 347 | + ignorable_versions=[], |
| 348 | + ) |
| 349 | + affected_purls = [ |
| 350 | + PackageURL(type=pkg_type, namespace=pkg_namespace, name=pkg_name, version=version) |
| 351 | + for version in aff_vers |
| 352 | + ] |
| 353 | + |
| 354 | + unaffected_purls = [ |
| 355 | + PackageURL(type=pkg_type, namespace=pkg_namespace, name=pkg_name, version=version) |
| 356 | + for version in unaff_vers |
| 357 | + ] |
| 358 | + |
| 359 | + affected_packages: List[LegacyAffectedPackage] = nearest_patched_package( |
| 360 | + vulnerable_packages=affected_purls, resolved_packages=unaffected_purls |
| 361 | + ) |
| 362 | + |
| 363 | + for ( |
| 364 | + fixed_package, |
| 365 | + affected_packages, |
| 366 | + ) in get_affected_packages_by_patched_package(affected_packages).items(): |
| 367 | + yield Inference.from_advisory_data( |
| 368 | + advisory_data, # We are getting all valid versions to get this inference |
| 369 | + confidence=100, |
| 370 | + affected_purls=affected_packages, |
| 371 | + fixed_purl=fixed_package, |
| 372 | + ) |
0 commit comments