|
12 | 12 | import logging |
13 | 13 | import os |
14 | 14 | import shutil |
15 | | -import tempfile |
16 | 15 | import traceback |
17 | 16 | import xml.etree.ElementTree as ET |
18 | 17 | from pathlib import Path |
|
23 | 22 | from typing import Set |
24 | 23 | from typing import Tuple |
25 | 24 |
|
26 | | -from binaryornot.helpers import is_binary_string |
27 | | -from git import DiffIndex |
28 | | -from git import Repo |
| 25 | +from fetchcode.vcs import fetch_via_vcs |
29 | 26 | from license_expression import Licensing |
30 | 27 | from packageurl import PackageURL |
31 | 28 | from univers.version_range import VersionRange |
@@ -71,8 +68,8 @@ class Reference: |
71 | 68 | severities: List[VulnerabilitySeverity] = dataclasses.field(default_factory=list) |
72 | 69 |
|
73 | 70 | def __post_init__(self): |
74 | | - if not self.url: |
75 | | - raise TypeError("Reference must have a url") |
| 71 | + if not any([self.url, self.reference_id]): |
| 72 | + raise TypeError |
76 | 73 |
|
77 | 74 | def normalized(self): |
78 | 75 | severities = sorted(self.severities) |
@@ -312,213 +309,38 @@ def advisory_data(self) -> Iterable[AdvisoryData]: |
312 | 309 | raise NotImplementedError |
313 | 310 |
|
314 | 311 |
|
315 | | -@dataclasses.dataclass |
316 | | -class GitConfig: |
317 | | - repository_url: str |
318 | | - branch: Optional[str] = None |
319 | | - create_working_directory: bool = True |
320 | | - remove_working_directory: bool = True |
321 | | - working_directory: Optional[str] = "" |
322 | | - last_run_date: Optional[str] = None |
323 | | - cutoff_date: Optional[str] = None |
| 312 | +class ForkError(Exception): |
| 313 | + pass |
324 | 314 |
|
325 | 315 |
|
326 | | -# TODO: Needs rewrite |
327 | 316 | class GitImporter(Importer): |
328 | | - def __init__(self, config, cutoff_timestamp): |
| 317 | + def __init__(self, repo_url): |
329 | 318 | super().__init__() |
330 | | - self.config = config |
331 | | - self.cutoff_timestamp = cutoff_timestamp |
332 | | - |
333 | | - self._ensure_working_directory() |
334 | | - self._ensure_repository() |
335 | | - |
336 | | - def validate_configuration(self) -> None: |
337 | | - if not self.config.create_working_directory and self.config.working_directory is None: |
338 | | - self.error( |
339 | | - '"create_working_directory" is not set but "working_directory" is set to ' |
340 | | - "the default, which calls tempfile.mkdtemp()" |
341 | | - ) |
| 319 | + self.repo_url = repo_url |
| 320 | + self.vcs_response = None |
342 | 321 |
|
343 | | - if not self.config.create_working_directory and not os.path.exists( |
344 | | - self.config.working_directory |
345 | | - ): |
346 | | - self.error( |
347 | | - '"working_directory" does not contain an existing directory and' |
348 | | - '"create_working_directory" is not set' |
349 | | - ) |
| 322 | + def __enter__(self): |
| 323 | + super().__enter__() |
| 324 | + self.clone() |
| 325 | + return self |
350 | 326 |
|
351 | | - if not self.config.remove_working_directory and self.config.working_directory is None: |
352 | | - self.error( |
353 | | - '"remove_working_directory" is not set and "working_directory" is set to ' |
354 | | - "the default, which calls tempfile.mkdtemp()" |
355 | | - ) |
| 327 | + def __exit__(self): |
| 328 | + self.vcs_response.delete() |
356 | 329 |
|
357 | | - def __exit__(self, exc_type, exc_val, exc_tb): |
358 | | - if self.config.remove_working_directory: |
359 | | - shutil.rmtree(self.config.working_directory) |
| 330 | + def clone(self): |
| 331 | + try: |
| 332 | + self.vcs_response = fetch_via_vcs(self.repo_url) |
| 333 | + except Exception as e: |
| 334 | + msg = f"Failed to fetch {self.repo_url} via vcs: {e}" |
| 335 | + logger.error(msg) |
| 336 | + raise ForkError(msg) from e |
360 | 337 |
|
361 | | - def file_changes( |
362 | | - self, |
363 | | - subdir: str = None, |
364 | | - recursive: bool = False, |
365 | | - file_ext: Optional[str] = None, |
366 | | - ) -> Tuple[Set[str], Set[str]]: |
| 338 | + def advisory_data(self) -> Iterable[AdvisoryData]: |
367 | 339 | """ |
368 | | - Returns all added and modified files since last_run_date or cutoff_date (whichever is more |
369 | | - recent). |
370 | | - :param subdir: filter by files in this directory |
371 | | - :param recursive: whether to include files in subdirectories |
372 | | - :param file_ext: filter files by this extension |
373 | | - :return: The first set contains (absolute paths to) added files, the second one modified |
374 | | - files |
| 340 | + Return AdvisoryData objects corresponding to the data being imported |
375 | 341 | """ |
376 | | - if subdir is None: |
377 | | - working_dir = self.config.working_directory |
378 | | - else: |
379 | | - working_dir = os.path.join(self.config.working_directory, subdir) |
380 | | - |
381 | | - path = Path(working_dir) |
382 | | - |
383 | | - if self.config.last_run_date is None and self.config.cutoff_date is None: |
384 | | - if recursive: |
385 | | - glob = "**/*" |
386 | | - else: |
387 | | - glob = "*" |
388 | | - |
389 | | - if file_ext: |
390 | | - glob = f"{glob}.{file_ext}" |
391 | | - |
392 | | - return {str(p) for p in path.glob(glob) if p.is_file()}, set() |
393 | | - |
394 | | - return self._collect_file_changes(subdir=subdir, recursive=recursive, file_ext=file_ext) |
395 | | - |
396 | | - def _collect_file_changes( |
397 | | - self, |
398 | | - subdir: Optional[str], |
399 | | - recursive: bool, |
400 | | - file_ext: Optional[str], |
401 | | - ) -> Tuple[Set[str], Set[str]]: |
402 | | - |
403 | | - added_files, updated_files = set(), set() |
404 | | - |
405 | | - # find the most ancient commit we need to diff with |
406 | | - cutoff_commit = None |
407 | | - for commit in self._repo.iter_commits(self._repo.head): |
408 | | - if commit.committed_date < self.cutoff_timestamp: |
409 | | - break |
410 | | - cutoff_commit = commit |
411 | | - |
412 | | - if cutoff_commit is None: |
413 | | - return added_files, updated_files |
414 | | - |
415 | | - def _is_binary(d: DiffIndex): |
416 | | - return is_binary_string(d.b_blob.data_stream.read(1024)) |
417 | | - |
418 | | - for d in cutoff_commit.diff(self._repo.head.commit): |
419 | | - if not _include_file(d.b_path, subdir, recursive, file_ext) or _is_binary(d): |
420 | | - continue |
421 | | - |
422 | | - abspath = os.path.join(self.config.working_directory, d.b_path) |
423 | | - if d.new_file: |
424 | | - added_files.add(abspath) |
425 | | - elif d.a_blob and d.b_blob: |
426 | | - if d.a_path != d.b_path: |
427 | | - # consider moved files as added |
428 | | - added_files.add(abspath) |
429 | | - elif d.a_blob != d.b_blob: |
430 | | - updated_files.add(abspath) |
431 | | - |
432 | | - # Any file that has been added and then updated inside the window of the git history we |
433 | | - # looked at, should be considered "added", not "updated", since it does not exist in the |
434 | | - # database yet. |
435 | | - updated_files = updated_files - added_files |
436 | | - |
437 | | - return added_files, updated_files |
438 | | - |
439 | | - def _ensure_working_directory(self) -> None: |
440 | | - if self.config.working_directory is None: |
441 | | - self.config.working_directory = tempfile.mkdtemp() |
442 | | - elif self.config.create_working_directory and not os.path.exists( |
443 | | - self.config.working_directory |
444 | | - ): |
445 | | - os.mkdir(self.config.working_directory) |
446 | | - |
447 | | - def _ensure_repository(self) -> None: |
448 | | - if not os.path.exists(os.path.join(self.config.working_directory, ".git")): |
449 | | - self._clone_repository() |
450 | | - return |
451 | | - self._repo = Repo(self.config.working_directory) |
452 | | - |
453 | | - if self.config.branch is None: |
454 | | - self.config.branch = str(self._repo.active_branch) |
455 | | - branch = self.config.branch |
456 | | - self._repo.head.reference = self._repo.heads[branch] |
457 | | - self._repo.head.reset(index=True, working_tree=True) |
458 | | - |
459 | | - remote = self._find_or_add_remote() |
460 | | - self._update_from_remote(remote, branch) |
461 | | - |
462 | | - def _clone_repository(self) -> None: |
463 | | - kwargs = {} |
464 | | - if self.config.branch: |
465 | | - kwargs["branch"] = self.config.branch |
466 | | - |
467 | | - self._repo = Repo.clone_from( |
468 | | - self.config.repository_url, self.config.working_directory, **kwargs |
469 | | - ) |
470 | | - |
471 | | - def _find_or_add_remote(self): |
472 | | - remote = None |
473 | | - for r in self._repo.remotes: |
474 | | - if r.url == self.config.repository_url: |
475 | | - remote = r |
476 | | - break |
477 | | - |
478 | | - if remote is None: |
479 | | - remote = self._repo.create_remote( |
480 | | - "added_by_vulnerablecode", url=self.config.repository_url |
481 | | - ) |
482 | | - |
483 | | - return remote |
484 | | - |
485 | | - def _update_from_remote(self, remote, branch) -> None: |
486 | | - fetch_info = remote.fetch() |
487 | | - if len(fetch_info) == 0: |
488 | | - return |
489 | | - branch = self._repo.branches[branch] |
490 | | - branch.set_reference(remote.refs[branch.name]) |
491 | | - self._repo.head.reset(index=True, working_tree=True) |
492 | | - |
493 | | - def advisory_data(self): |
494 | 342 | raise NotImplementedError |
495 | 343 |
|
496 | | - def error(self, param): |
497 | | - pass |
498 | | - |
499 | | - |
500 | | -def _include_file( |
501 | | - path: str, |
502 | | - subdir: Optional[str] = None, |
503 | | - recursive: bool = False, |
504 | | - file_ext: Optional[str] = None, |
505 | | -) -> bool: |
506 | | - match = True |
507 | | - |
508 | | - if subdir: |
509 | | - if not subdir.endswith(os.path.sep): |
510 | | - subdir = f"{subdir}{os.path.sep}" |
511 | | - |
512 | | - match = match and path.startswith(subdir) |
513 | | - |
514 | | - if not recursive: |
515 | | - match = match and (os.path.sep not in path[len(subdir or "") :]) |
516 | | - |
517 | | - if file_ext: |
518 | | - match = match and path.endswith(f".{file_ext}") |
519 | | - |
520 | | - return match |
521 | | - |
522 | 344 |
|
523 | 345 | # TODO: Needs rewrite |
524 | 346 | class OvalImporter(Importer): |
|
0 commit comments