|
| 1 | +# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. |
| 2 | +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. |
| 3 | + |
| 4 | +"""This module contains the base class and core data models for repository verification.""" |
| 5 | +import abc |
| 6 | +import logging |
| 7 | +import os |
| 8 | +from collections import deque |
| 9 | +from dataclasses import dataclass |
| 10 | +from enum import Enum |
| 11 | +from pathlib import Path |
| 12 | + |
| 13 | +from macaron.slsa_analyzer.build_tool import BaseBuildTool |
| 14 | + |
| 15 | +logger = logging.getLogger(__name__) |
| 16 | + |
| 17 | + |
| 18 | +def find_file_in_repo(root_dir: Path, filename: str) -> Path | None: |
| 19 | + """Find the highest level file with a given name in a local repository. |
| 20 | +
|
| 21 | + This function ignores certain paths that are not under the main source code directories. |
| 22 | +
|
| 23 | + Parameters |
| 24 | + ---------- |
| 25 | + root_dir : Path |
| 26 | + The root directory of the repository. |
| 27 | + filename : str |
| 28 | + The name of the file to search for. |
| 29 | +
|
| 30 | + Returns |
| 31 | + ------- |
| 32 | + Path | None |
| 33 | + The path to the file if it exists, otherwise |
| 34 | + """ |
| 35 | + # TODO: Consider using BaseBuildTool.get_build_dirs. |
| 36 | + # + Refactor 'get_build_dirs' to skip certain directories |
| 37 | + # that are most likely not part of the main codebase (e.g., sample). |
| 38 | + # + Need to find a way to look for other |
| 39 | + # files (e.g., gradle.properties) for the purpose of repo verification |
| 40 | + # without breaking the current logic of finding build directories. |
| 41 | + # + Add the capability to return the content/path of the file. |
| 42 | + if not os.path.isdir(root_dir): |
| 43 | + return None |
| 44 | + |
| 45 | + queue: deque[Path] = deque() |
| 46 | + queue.append(Path(root_dir)) |
| 47 | + while queue: |
| 48 | + current_dir = queue.popleft() |
| 49 | + |
| 50 | + # Don't look through non-main directories. |
| 51 | + if any( |
| 52 | + keyword in current_dir.name.lower() |
| 53 | + for keyword in ["test", "example", "sample", "doc", "demo", "spec", "mock"] |
| 54 | + ): |
| 55 | + continue |
| 56 | + |
| 57 | + if Path(current_dir, filename).exists(): |
| 58 | + return Path(current_dir, filename) |
| 59 | + |
| 60 | + # Ignore symlinks to prevent potential infinite loop. |
| 61 | + sub_dirs = [Path(it) for it in current_dir.iterdir() if it.is_dir() and not it.is_symlink()] |
| 62 | + queue.extend(sub_dirs) |
| 63 | + |
| 64 | + return None |
| 65 | + |
| 66 | + |
| 67 | +class RepositoryVerificationStatus(str, Enum): |
| 68 | + """A class to store the status of the repo verification.""" |
| 69 | + |
| 70 | + #: We found evidence to prove that the repository can be linked back to the publisher of the artifact. |
| 71 | + PASSED = "passed" |
| 72 | + |
| 73 | + #: We found evidence showing that the repository is not the publisher of the artifact. |
| 74 | + FAILED = "failed" |
| 75 | + |
| 76 | + #: We could not find any evidence to prove or disprove that the repository can be linked back to the artifact. |
| 77 | + UNKNOWN = "unknown" |
| 78 | + |
| 79 | + |
| 80 | +@dataclass(frozen=True) |
| 81 | +class RepositoryVerificationResult: |
| 82 | + """A class to store the information about repository verification.""" |
| 83 | + |
| 84 | + #: The status of the repository verification. |
| 85 | + status: RepositoryVerificationStatus |
| 86 | + |
| 87 | + #: The reason for the verification result. |
| 88 | + reason: str |
| 89 | + |
| 90 | + #: The build tool used to build the package. |
| 91 | + build_tool: BaseBuildTool |
| 92 | + |
| 93 | + |
| 94 | +class RepoVerifierBase(abc.ABC): |
| 95 | + """The base class to verify whether a reported repository links back to the artifact.""" |
| 96 | + |
| 97 | + @property |
| 98 | + @abc.abstractmethod |
| 99 | + def build_tool(self) -> BaseBuildTool: |
| 100 | + """Define the build tool used to build the package.""" |
| 101 | + |
| 102 | + def __init__( |
| 103 | + self, |
| 104 | + namespace: str | None, |
| 105 | + name: str, |
| 106 | + version: str, |
| 107 | + reported_repo_url: str, |
| 108 | + reported_repo_fs: str, |
| 109 | + ): |
| 110 | + """Instantiate the class. |
| 111 | +
|
| 112 | + Parameters |
| 113 | + ---------- |
| 114 | + namespace : str |
| 115 | + The namespace of the artifact. |
| 116 | + name : str |
| 117 | + The name of the artifact. |
| 118 | + version : str |
| 119 | + The version of the artifact. |
| 120 | + reported_repo_url : str |
| 121 | + The URL of the repository reported by the publisher. |
| 122 | + reported_repo_fs : str |
| 123 | + The file system path of the reported repository. |
| 124 | + """ |
| 125 | + self.namespace = namespace |
| 126 | + self.name = name |
| 127 | + self.version = version |
| 128 | + self.reported_repo_url = reported_repo_url |
| 129 | + self.reported_repo_fs = reported_repo_fs |
| 130 | + |
| 131 | + @abc.abstractmethod |
| 132 | + def verify_repo(self) -> RepositoryVerificationResult: |
| 133 | + """Verify whether the repository links back to the artifact. |
| 134 | +
|
| 135 | + Returns |
| 136 | + ------- |
| 137 | + RepositoryVerificationResult |
| 138 | + The result of the repository verification |
| 139 | + """ |
0 commit comments