From 27b628152d697998aaeb56a50eb192c8fc404aea Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Fri, 15 Nov 2024 14:03:56 +0100 Subject: [PATCH] feat: provider structural resemblance --- prowler/__main__.py | 5 + prowler/compliance/github/__init__.py | 0 prowler/compliance/github/cis_1.0_github.json | 7 + prowler/lib/check/models.py | 13 ++ prowler/providers/common/provider.py | 8 + prowler/providers/github/PoC.py | 35 +---- prowler/providers/github/__init__.py | 0 prowler/providers/github/github_provider.py | 141 ++++++++++++++++++ .../github/lib/arguments/__init__.py | 0 .../github/lib/arguments/arguments.py | 23 +++ .../providers/github/lib/mutelist/__init__.py | 0 .../providers/github/lib/mutelist/mutelist.py | 17 +++ .../providers/github/lib/service/__init__.py | 0 .../providers/github/lib/service/service.py | 32 ++++ prowler/providers/github/models.py | 29 ++++ .../github/services/account/__init__.py | 0 .../services/account/account_service.py | 31 ++++ 17 files changed, 313 insertions(+), 28 deletions(-) create mode 100644 prowler/compliance/github/__init__.py create mode 100644 prowler/compliance/github/cis_1.0_github.json create mode 100644 prowler/providers/github/__init__.py create mode 100644 prowler/providers/github/github_provider.py create mode 100644 prowler/providers/github/lib/arguments/__init__.py create mode 100644 prowler/providers/github/lib/arguments/arguments.py create mode 100644 prowler/providers/github/lib/mutelist/__init__.py create mode 100644 prowler/providers/github/lib/mutelist/mutelist.py create mode 100644 prowler/providers/github/lib/service/__init__.py create mode 100644 prowler/providers/github/lib/service/service.py create mode 100644 prowler/providers/github/models.py create mode 100644 prowler/providers/github/services/account/__init__.py create mode 100644 prowler/providers/github/services/account/account_service.py diff --git a/prowler/__main__.py b/prowler/__main__.py index 71325e1f4a..b9af124f9c 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -75,6 +75,7 @@ from prowler.providers.azure.models import AzureOutputOptions from prowler.providers.common.provider import Provider from prowler.providers.common.quick_inventory import run_provider_quick_inventory from prowler.providers.gcp.models import GCPOutputOptions +from prowler.providers.github.models import GithubOutputOptions from prowler.providers.kubernetes.models import KubernetesOutputOptions @@ -257,6 +258,10 @@ def prowler(): output_options = KubernetesOutputOptions( args, bulk_checks_metadata, global_provider.identity ) + elif provider == "github": + output_options = GithubOutputOptions( + args, bulk_checks_metadata, global_provider.identity + ) # Run the quick inventory for the provider if available if hasattr(args, "quick_inventory") and args.quick_inventory: diff --git a/prowler/compliance/github/__init__.py b/prowler/compliance/github/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/compliance/github/cis_1.0_github.json b/prowler/compliance/github/cis_1.0_github.json new file mode 100644 index 0000000000..f5431f2d44 --- /dev/null +++ b/prowler/compliance/github/cis_1.0_github.json @@ -0,0 +1,7 @@ +{ + "Framework": "CIS", + "Version": "1.0", + "Provider": "Github", + "Description": "This CIS Benchmark provides prescriptive guidance for establishing a secure configuration posture for securing the Software Supply Chain.", + "Requirements": [] +} diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index 446440b6aa..615fa06cb9 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -502,3 +502,16 @@ def load_check_metadata(metadata_file: str) -> CheckMetadata: sys.exit(1) else: return check_metadata + + +@dataclass +class Check_Report_GitHub(Check_Report): + """Contains the GitHub Check's finding information.""" + + resource_name: str + resource_id: str + + def __init__(self, metadata): + super().__init__(metadata) + self.resource_name = "" + self.resource_id = "" diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 4e86ad42bb..dc1ed03401 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -211,6 +211,14 @@ class Provider(ABC): mutelist_path=arguments.mutelist_file, fixer_config=fixer_config, ) + elif "github" in provider_class_name.lower(): + provider_class( + personal_access_token=arguments.personal_access_token, + github_app=arguments.github_app, + oauth_app=arguments.oauth_app, + config_content=arguments.config_content, + config_path=arguments.config_file, + ) except TypeError as error: logger.critical( diff --git a/prowler/providers/github/PoC.py b/prowler/providers/github/PoC.py index 27ebdab61c..144cf18f3b 100644 --- a/prowler/providers/github/PoC.py +++ b/prowler/providers/github/PoC.py @@ -1,32 +1,11 @@ -import json +from os import getenv -import requests +from github import Auth, Github -TOKEN = "token" -ENDPOINT = "https://api.github.com/graphql" +TOKEN = getenv("GITHUB_PERSONAL_ACCESS_TOKEN") -headers = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"} +auth = Auth.Token(TOKEN) +g = Github(auth=auth) -query = """ -{ - user(login: "HugoPBrito") { - login - twoFactorAuthentication { - enabled - } - } -} -""" - -response = requests.post(ENDPOINT, headers=headers, json={"query": query}) - -if response.status_code == 200: - print(json.dumps(response.json(), indent=2)) - - data = response.json() - two_factor_authentication = data["data"]["user"]["twoFactorAuthentication"][ - "enabled" - ] - print(two_factor_authentication) -else: - print(f"Error {response.status_code}: {response.text}") +for repo in g.get_user().get_repos(): + print(repo) diff --git a/prowler/providers/github/__init__.py b/prowler/providers/github/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/github/github_provider.py b/prowler/providers/github/github_provider.py new file mode 100644 index 0000000000..1eb6609d50 --- /dev/null +++ b/prowler/providers/github/github_provider.py @@ -0,0 +1,141 @@ +from os import getenv + +from github import Auth, Github + +from prowler.config.config import ( + default_config_file_path, + load_and_validate_config_file, +) +from prowler.lib.logger import logger +from prowler.providers.common.models import Audit_Metadata +from prowler.providers.common.provider import Provider +from prowler.providers.github.models import GithubIdentityInfo, GithubSession + + +class GithubProvider(Provider): + _type: str = "github" + _session: GithubSession + _identity: GithubIdentityInfo + _audit_config: dict + audit_metadata: Audit_Metadata + + def __init__( + self, + personal_access_token: bool = False, + github_app: bool = False, + oauth_app: bool = False, + config_content: dict = None, + config_path: str = None, + ): + """ + GitHub Provider constructor + + Args: + personal_access_token (str): GitHub token as authentication method + github_app (bool): GitHub App as authentication method + oauth_app (bool): OAuth App as authentication method + config_content (dict): Configuration content + config_path (str): Configuration path + """ + logger.info("Instantiating GitHub Provider...") + self._session = self.setup_session( + personal_access_token, + github_app, + oauth_app, + ) + self._identity = self.setup_identity( + personal_access_token, + github_app, + oauth_app, + ) + self._audit_config = {} + if config_content: + self._audit_config = config_content + else: + if not config_path: + config_path = default_config_file_path + self._audit_config = load_and_validate_config_file(self._type, config_path) + + @property + def identity(self): + """Returns the identity information for the GitHub provider.""" + return self._identity + + @property + def session(self): + """Returns the session object for the GitHub provider.""" + return self._session + + @property + def type(self): + """Returns the type of the GitHub provider.""" + return self._type + + @property + def audit_config(self): + """Returns the audit configuration for the GitHub provider.""" + return self._audit_config + + @staticmethod + def setup_session( + self, + personal_access_token: bool = False, + github_app: bool = False, + oauth_app: bool = False, + ) -> GithubSession: + """ + Returns the GitHub headers responsible authenticating API calls. + + Args: + personal_access_token (str): Flag indicating whether to use GitHub personal access token as authentication method. + github_app (bool): Flag indicating whether to use GitHub App as authentication method. + oauth_app (bool): Flag indicating whether to use OAuth App as authentication method. + + Returns: + GithubSession: Authenticated session for API requests. + """ + + if personal_access_token: + token = getenv("GITHUB_PERSONAL_ACCESS_TOKEN") + elif github_app: + token = getenv("GITHUB_APP_TOKEN") + elif oauth_app: + token = getenv("GITHUB_OAUTH_TOKEN") + else: + raise ValueError( + "A GitHub API token of some kind is required to initialize GitHub provider." + ) + + credentials = GithubSession(token=Auth.Token(token=token)) + + return credentials + + @staticmethod + def setup_identity( + self, + personal_access_token: bool = False, + github_app: bool = False, + oauth_app: bool = False, + ) -> GithubIdentityInfo: + """ + Returns the GitHub identity information + + Args: + personal_access_token (str): Flag indicating whether to use GitHub personal access token as authentication method. + github_app (bool): Flag indicating whether to use GitHub App as authentication method. + oauth_app (bool): Flag indicating whether to use OAuth App as authentication method. + + Returns: + GithubIdentityInfo: An instance of GithubIdentityInfo containing the identity information. + """ + credentials = self.session + identity = GithubIdentityInfo() + + if personal_access_token or github_app or oauth_app: + auth = Auth.Token(credentials.token) + g = Github(auth=auth) + identity.account_name = g.get_user().login + identity.account_id = g.get_user().id + identity.account_url = g.get_user().url + + return identity diff --git a/prowler/providers/github/lib/arguments/__init__.py b/prowler/providers/github/lib/arguments/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/github/lib/arguments/arguments.py b/prowler/providers/github/lib/arguments/arguments.py new file mode 100644 index 0000000000..c44b847ae3 --- /dev/null +++ b/prowler/providers/github/lib/arguments/arguments.py @@ -0,0 +1,23 @@ +def init_parser(self): + """Init the Github Provider CLI parser""" + github_parser = self.subparsers.add_parser( + "github", parents=[self.common_providers_parser], help="GitHub Provider" + ) + # Authentication Modes + github_auth_subparser = github_parser.add_argument_group("Authentication Modes") + github_auth_modes_group = github_auth_subparser.add_mutually_exclusive_group() + github_auth_modes_group.add_argument( + "--pat-auth", + action="store_true", + help="Use Personal Access Token to log in against GitHub", + ) + github_auth_modes_group.add_argument( + "--oauth-auth", + action="store_true", + help="Use Oauth app token to log in against GitHub", + ) + github_auth_modes_group.add_argument( + "--app-auth", + action="store_true", + help="Use GitHub app token to log in against GitHub", + ) diff --git a/prowler/providers/github/lib/mutelist/__init__.py b/prowler/providers/github/lib/mutelist/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/github/lib/mutelist/mutelist.py b/prowler/providers/github/lib/mutelist/mutelist.py new file mode 100644 index 0000000000..e0b46449c1 --- /dev/null +++ b/prowler/providers/github/lib/mutelist/mutelist.py @@ -0,0 +1,17 @@ +from prowler.lib.check.models import Check_Report_GitHub +from prowler.lib.mutelist.mutelist import Mutelist +from prowler.lib.outputs.utils import unroll_dict, unroll_tags + + +class GitHubMutelist(Mutelist): + def is_finding_muted( + self, + finding: Check_Report_GitHub, + ) -> bool: + return self.is_muted( + finding.subscription, + finding.check_metadata.CheckID, + finding.location, + finding.resource_name, + unroll_dict(unroll_tags(finding.resource_tags)), + ) diff --git a/prowler/providers/github/lib/service/__init__.py b/prowler/providers/github/lib/service/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/github/lib/service/service.py b/prowler/providers/github/lib/service/service.py new file mode 100644 index 0000000000..f1a30785ea --- /dev/null +++ b/prowler/providers/github/lib/service/service.py @@ -0,0 +1,32 @@ +from github import Auth, Github + +from prowler.lib.logger import logger +from prowler.providers.github.github_provider import GithubProvider + + +class GithubService: + def __init__( + self, + service: str, + provider: GithubProvider, + ): + self.clients = self.__set_clients__( + provider.identity, + provider.session, + service, + ) + + self.audit_config = provider.audit_config + self.fixer_config = provider.fixer_config + + def __set_clients__(self, session): + client = {} + try: + auth = Auth.Token(session.token) + client = Github(auth=auth) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + else: + return client diff --git a/prowler/providers/github/models.py b/prowler/providers/github/models.py new file mode 100644 index 0000000000..6c42ec50c0 --- /dev/null +++ b/prowler/providers/github/models.py @@ -0,0 +1,29 @@ +from pydantic import BaseModel + +from prowler.config.config import output_file_timestamp +from prowler.providers.common.models import ProviderOutputOptions + + +class GithubIdentityInfo(BaseModel): + account_name: str + account_id: str + account_url: str + + +class GithubSession(BaseModel): + token: str + + +class GithubOutputOptions(ProviderOutputOptions): + def __init__(self, arguments, bulk_checks_metadata, identity): + # First call ProviderOutputOptions init + super().__init__(arguments, bulk_checks_metadata) + # TODO move the below if to ProviderOutputOptions + # Check if custom output filename was input, if not, set the default + if ( + not hasattr(arguments, "output_filename") + or arguments.output_filename is None + ): + self.output_filename = f"prowler-output-{identity.context.replace(':', '_').replace('/', '_')}-{output_file_timestamp}" + else: + self.output_filename = arguments.output_filename diff --git a/prowler/providers/github/services/account/__init__.py b/prowler/providers/github/services/account/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/github/services/account/account_service.py b/prowler/providers/github/services/account/account_service.py new file mode 100644 index 0000000000..707cb7fa9a --- /dev/null +++ b/prowler/providers/github/services/account/account_service.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.github.github_provider import GithubProvider +from prowler.providers.github.lib.service.service import GithubService + + +class GitHubAccount(GithubService): + def __init__(self, provider: GithubProvider): + super().__init__("GitHubAccount", provider) + self.account = Account( + name=provider.identity.account_name, id=provider.identity.account_id + ) + self._get_ssh_keys_status() + + def _get_ssh_keys_status(self): + try: + user = self.client.get_user() + keys = self.client.get_user_keys(user.login) + self.account.ssh_keys = keys.totalCount > 0 + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + +class Account(BaseModel): + name: str + id: str + ssh_keys: bool = False