diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index d2c599adb7..8ed12f4bce 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -242,6 +242,7 @@ class Provider(ABC): provider_class( personal_access_token=arguments.personal_access_token, oauth_app_token=arguments.oauth_app_token, + github_app_key=arguments.github_app_key, github_app_key_path=arguments.github_app_key_path, github_app_id=arguments.github_app_id, mutelist_path=arguments.mutelist_file, diff --git a/prowler/providers/github/github_provider.py b/prowler/providers/github/github_provider.py index 728e47c735..5b5e227a45 100644 --- a/prowler/providers/github/github_provider.py +++ b/prowler/providers/github/github_provider.py @@ -102,6 +102,7 @@ class GithubProvider(Provider): # Authentication credentials personal_access_token: str = "", oauth_app_token: str = "", + github_app_key: str = "", github_app_key_path: str = "", github_app_key_content: str = "", github_app_id: int = 0, @@ -120,8 +121,9 @@ class GithubProvider(Provider): Args: personal_access_token (str): GitHub personal access token. oauth_app_token (str): GitHub OAuth App token. + github_app_key (str): GitHub App key content. github_app_key_path (str): Path to GitHub App private key file. - github_app_key_content (str): GitHub App private key content. + github_app_key_content (str): GitHub App private key content (legacy parameter). github_app_id (int): GitHub App ID. config_path (str): Path to the audit configuration file. config_content (dict): Audit configuration content. @@ -147,6 +149,7 @@ class GithubProvider(Provider): personal_access_token, oauth_app_token, github_app_id, + github_app_key, github_app_key_path, github_app_key_content, ) @@ -156,7 +159,9 @@ class GithubProvider(Provider): self._auth_method = "Personal Access Token" elif oauth_app_token: self._auth_method = "OAuth App Token" - elif github_app_id and (github_app_key_path or github_app_key_content): + elif github_app_id and ( + github_app_key or github_app_key_path or github_app_key_content + ): self._auth_method = "GitHub App Token" elif environ.get("GITHUB_PERSONAL_ACCESS_TOKEN", ""): self._auth_method = "Environment Variable for Personal Access Token" @@ -250,6 +255,7 @@ class GithubProvider(Provider): personal_access_token: str = None, oauth_app_token: str = None, github_app_id: int = 0, + github_app_key: str = None, github_app_key_path: str = None, github_app_key_content: str = None, ) -> GithubSession: @@ -260,8 +266,9 @@ class GithubProvider(Provider): personal_access_token (str): GitHub personal access token. oauth_app_token (str): GitHub OAuth App token. github_app_id (int): GitHub App ID. + github_app_key (str): GitHub App key content. github_app_key_path (str): Path to GitHub App private key file. - github_app_key_content (str): GitHub App private key content. + github_app_key_content (str): GitHub App private key content (legacy parameter). Returns: GithubSession: Authenticated session token for API requests. """ @@ -278,11 +285,35 @@ class GithubProvider(Provider): elif oauth_app_token: session_token = oauth_app_token - elif github_app_id and (github_app_key_path or github_app_key_content): + elif github_app_id and ( + github_app_key or github_app_key_path or github_app_key_content + ): app_id = github_app_id if github_app_key_path: - with open(github_app_key_path, "r") as rsa_key: - app_key = rsa_key.read() + try: + with open(github_app_key_path, "r") as rsa_key: + app_key = rsa_key.read() + except OSError as e: + if e.errno == 63: + raise GithubEnvironmentVariableError( + file=os.path.basename(__file__), + message="--github-app-key-path expects a file path, not key content. Use --github-app-key for key content instead.", + ) + else: + raise GithubEnvironmentVariableError( + file=os.path.basename(__file__), + message=f"Could not read GitHub App key file '{github_app_key_path}': {e.message}", + ) + elif github_app_key: + if github_app_key.startswith("-----BEGIN"): + app_key = format_rsa_key(github_app_key) + elif os.path.isfile(github_app_key): + raise GithubEnvironmentVariableError( + file=os.path.basename(__file__), + message="--github-app-key expects key content, not a file path. Use --github-app-key-path for file paths instead.", + ) + else: + app_key = format_rsa_key(github_app_key) else: app_key = format_rsa_key(github_app_key_content) @@ -303,22 +334,27 @@ class GithubProvider(Provider): if not session_token: # APP logger.info( - "Looking for GITHUB_APP_ID and GITHUB_APP_KEY environment variables as user has not provided any token...." + "Looking for GitHub App environment variables as user has not provided any token...." ) app_id = environ.get("GITHUB_APP_ID", "") - env_key = environ.get("GITHUB_APP_KEY", "") - if app_id and env_key: - if env_key.startswith("-----BEGIN"): - app_key = format_rsa_key(env_key) - elif os.path.isfile(env_key): - with open(env_key, "r") as rsa_key: - app_key = rsa_key.read() - else: - raise GithubEnvironmentVariableError( - file=os.path.basename(__file__), - message="GITHUB_APP_KEY must contain either RSA key content (starting with -----BEGIN) or a valid file path.", - ) + app_key_path = environ.get("GITHUB_APP_KEY_PATH", "") + if app_key_path: + with open(app_key_path, "r") as rsa_key: + app_key = rsa_key.read() + else: + env_key = environ.get("GITHUB_APP_KEY", "") + if env_key: + if env_key.startswith("-----BEGIN"): + app_key = format_rsa_key(env_key) + elif os.path.isfile(env_key): + with open(env_key, "r") as rsa_key: + app_key = rsa_key.read() + else: + raise GithubEnvironmentVariableError( + file=os.path.basename(__file__), + message="GITHUB_APP_KEY must contain either RSA key content (starting with -----BEGIN) or a valid file path.", + ) if not session_token and not (app_id and app_key): raise GithubEnvironmentVariableError( @@ -528,6 +564,7 @@ class GithubProvider(Provider): def test_connection( personal_access_token: str = "", oauth_app_token: str = "", + github_app_key: str = "", github_app_key_path: str = "", github_app_key_content: str = "", github_app_id: int = 0, @@ -541,8 +578,9 @@ class GithubProvider(Provider): Args: personal_access_token (str): GitHub personal access token. oauth_app_token (str): GitHub OAuth App token. + github_app_key (str): GitHub App key content. github_app_key_path (str): Path to GitHub App private key file. - github_app_key_content (str): GitHub App private key content. + github_app_key_content (str): GitHub App private key content (legacy parameter). github_app_id (int): GitHub App ID. raise_on_exception (bool): Flag indicating whether to raise an exception if the connection fails. provider_id (str): The provider ID, in this case it's the GitHub organization/username. @@ -573,6 +611,7 @@ class GithubProvider(Provider): personal_access_token=personal_access_token, oauth_app_token=oauth_app_token, github_app_id=github_app_id, + github_app_key=github_app_key, github_app_key_path=github_app_key_path, github_app_key_content=github_app_key_content, ) diff --git a/prowler/providers/github/lib/arguments/arguments.py b/prowler/providers/github/lib/arguments/arguments.py index 387bea6ddd..ca4cf14493 100644 --- a/prowler/providers/github/lib/arguments/arguments.py +++ b/prowler/providers/github/lib/arguments/arguments.py @@ -30,9 +30,15 @@ def init_parser(self): metavar="GITHUB_APP_ID", ) github_auth_subparser.add_argument( - "--github-app-key-path", "--github-app-key", nargs="?", + help="GitHub App Key content (PEM format) to log in against GitHub", + default=None, + metavar="GITHUB_APP_KEY_CONTENT", + ) + github_auth_subparser.add_argument( + "--github-app-key-path", + nargs="?", help="Path to GitHub App private key file", default=None, metavar="GITHUB_APP_KEY_PATH", @@ -55,3 +61,31 @@ def init_parser(self): default=None, metavar="ORGANIZATION", ) + + +def validate_arguments(arguments) -> tuple[bool, str]: + """ + Validate GitHub provider arguments + + Args: + arguments: Parsed command line arguments + + Returns: + tuple[bool, str]: (is_valid, error_message) + """ + + if arguments.github_app_key and arguments.github_app_key_path: + return ( + False, + "Cannot specify both --github-app-key and --github-app-key-path simultaneously", + ) + + if arguments.github_app_id and not ( + arguments.github_app_key or arguments.github_app_key_path + ): + return ( + False, + "GitHub App ID requires either --github-app-key or --github-app-key-path", + ) + + return True, "" diff --git a/tests/providers/github/github_provider_test.py b/tests/providers/github/github_provider_test.py index 9f71556a0d..e45047f4ab 100644 --- a/tests/providers/github/github_provider_test.py +++ b/tests/providers/github/github_provider_test.py @@ -217,7 +217,7 @@ class TestGitHubProvider: ), ): connection = GithubProvider.test_connection( - github_app_id=APP_ID, github_app_key_path=APP_KEY + github_app_id=APP_ID, github_app_key_content=APP_KEY ) assert isinstance(connection, Connection) diff --git a/tests/providers/github/lib/arguments/github_arguments_test.py b/tests/providers/github/lib/arguments/github_arguments_test.py index bc034cbf53..ea05950494 100644 --- a/tests/providers/github/lib/arguments/github_arguments_test.py +++ b/tests/providers/github/lib/arguments/github_arguments_test.py @@ -61,7 +61,7 @@ class Test_GitHubArguments: arguments.init_parser(mock_github_args) # Verify authentication arguments were added - assert self.mock_auth_group.add_argument.call_count == 4 + assert self.mock_auth_group.add_argument.call_count == 5 # Check that all authentication arguments are present calls = self.mock_auth_group.add_argument.call_args_list @@ -76,8 +76,8 @@ class Test_GitHubArguments: assert "--personal-access-token" in auth_args assert "--oauth-app-token" in auth_args assert "--github-app-id" in auth_args - assert "--github-app-key-path" in auth_args assert "--github-app-key" in auth_args + assert "--github-app-key-path" in auth_args def test_init_parser_adds_scoping_arguments(self): """Test that init_parser adds all scoping arguments""" @@ -309,3 +309,81 @@ class Test_GitHubArguments_Integration: assert args.personal_access_token == "test-token" assert args.repository == [] assert args.organization == [] + + +class Test_GitHubArguments_Validation: + def test_validate_arguments_both_key_methods_provided(self): + """Test validation fails when both key methods are provided""" + from argparse import Namespace + + args = Namespace() + args.github_app_key = "key-content" + args.github_app_key_path = "/path/to/key.pem" + args.github_app_id = "12345" + + is_valid, error_msg = arguments.validate_arguments(args) + + assert not is_valid + assert ( + "Cannot specify both --github-app-key and --github-app-key-path simultaneously" + in error_msg + ) + + def test_validate_arguments_app_id_without_key(self): + """Test validation fails when app ID is provided without any key""" + from argparse import Namespace + + args = Namespace() + args.github_app_key = None + args.github_app_key_path = None + args.github_app_id = "12345" + + is_valid, error_msg = arguments.validate_arguments(args) + + assert not is_valid + assert ( + "GitHub App ID requires either --github-app-key or --github-app-key-path" + in error_msg + ) + + def test_validate_arguments_valid_key_content(self): + """Test validation passes with valid key content""" + from argparse import Namespace + + args = Namespace() + args.github_app_key = "-----BEGIN RSA PRIVATE KEY-----" + args.github_app_key_path = None + args.github_app_id = "12345" + + is_valid, error_msg = arguments.validate_arguments(args) + + assert is_valid + assert error_msg == "" + + def test_validate_arguments_valid_key_path(self): + """Test validation passes with valid key path""" + from argparse import Namespace + + args = Namespace() + args.github_app_key = None + args.github_app_key_path = "/path/to/key.pem" + args.github_app_id = "12345" + + is_valid, error_msg = arguments.validate_arguments(args) + + assert is_valid + assert error_msg == "" + + def test_validate_arguments_no_app_auth(self): + """Test validation passes when no app authentication is used""" + from argparse import Namespace + + args = Namespace() + args.github_app_key = None + args.github_app_key_path = None + args.github_app_id = None + + is_valid, error_msg = arguments.validate_arguments(args) + + assert is_valid + assert error_msg == ""