Compare commits

...

14 Commits

Author SHA1 Message Date
Daniel Barranquero
dd3ab3a257 Merge branch 'master' into prwlr-7751-github-app-authentication-incorrectly-handles-key-parameter-and-environment-variables 2025-11-13 10:00:44 +01:00
Daniel Barranquero
73f7f1126d merge branch 'master' into prwlr-7751-github-app-authentication-incorrectly-handles-key-parameter-and-environment-variables 2025-10-09 14:40:46 +02:00
Daniel Barranquero
9240a04aca fix: apply suggestions 2025-10-09 14:18:08 +02:00
Daniel Barranquero
1e813dbace feat(docs): add key path to github docs 2025-10-03 09:50:00 +02:00
Daniel Barranquero
a0c00ab2e7 feat(github): add unit tests 2025-10-03 09:32:01 +02:00
Daniel Barranquero
916eb0ff23 feat(github): handle separately path and content of github app key 2025-10-02 19:13:17 +02:00
Daniel Barranquero
efbf641bb3 merge branch 'master' into prwlr-7751-github-app-authentication-incorrectly-handles-key-parameter-and-environment-variables 2025-10-02 12:23:24 +02:00
Andoni A.
8cd83f9ea1 revert flag changes 2025-08-01 16:07:16 +02:00
Andoni A.
b1cfc2af7e Merge branch 'master' into prwlr-7751-github-app-authentication-incorrectly-handles-key-parameter-and-environment-variables 2025-08-01 15:34:59 +02:00
Andoni A.
e580f79523 revision 2025-08-01 08:46:47 +02:00
Andoni A.
100038e80d fix(github): rename github_app_key_content to keep API compatibility -- update tests 2025-08-01 08:21:44 +02:00
Andoni A.
4866212f2d fix(github): rename github_app_key_content to keep API compatibility 2025-08-01 08:15:58 +02:00
Andoni A.
110f98e49d chore: update CHANGELOG 2025-07-31 20:17:13 +02:00
Andoni A.
31d0db2d2c fix(github): separate --github-app-key and --github-app-key-path CLI parameters 2025-07-31 20:06:27 +02:00
8 changed files with 558 additions and 39 deletions

View File

@@ -217,7 +217,8 @@ Prowler enables security scanning of your **GitHub account**, including **Reposi
prowler github --oauth-app-token oauth_token
# GitHub App Credentials:
prowler github --github-app-id app_id --github-app-key app_key
prowler github --github-app-id app_id --github-app-key-path path/to/app_key.pem
prowler github --github-app-id app_id --github-app-key $APP_KEY_CONTENT
```
<Note>
@@ -225,7 +226,8 @@ Prowler enables security scanning of your **GitHub account**, including **Reposi
1. `GITHUB_PERSONAL_ACCESS_TOKEN`
2. `OAUTH_APP_TOKEN`
3. `GITHUB_APP_ID` and `GITHUB_APP_KEY`
3. `GITHUB_APP_ID` and `GITHUB_APP_KEY_PATH`
4. `GITHUB_APP_ID` and `GITHUB_APP_KEY`
</Note>
## Infrastructure as Code (IaC)

View File

@@ -60,7 +60,8 @@ If no login method is explicitly provided, Prowler will automatically attempt to
1. `GITHUB_PERSONAL_ACCESS_TOKEN`
2. `GITHUB_OAUTH_APP_TOKEN`
3. `GITHUB_APP_ID` and `GITHUB_APP_KEY` (where the key is the content of the private key file)
3. `GITHUB_APP_ID` and `GITHUB_APP_KEY_PATH` (where the key path is the path to the private key file)
4. `GITHUB_APP_ID` and `GITHUB_APP_KEY` (where the key is the content of the private key file)
<Note>
Ensure the corresponding environment variables are set up before running Prowler for automatic detection when not specifying the login method.
@@ -88,5 +89,6 @@ prowler github --oauth-app-token oauth_token
Use GitHub App credentials by specifying the App ID and the private key path.
```console
prowler github --github-app-id app_id --github-app-key-path app_key_path
prowler github --github-app-id app_id --github-app-key-path path/to/app_key.pem
prowler github --github-app-id app_id --github-app-key $APP_KEY_CONTENT
```

View File

@@ -50,7 +50,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update oraclecloud cloudguard service metadata to new format [(#9223)](https://github.com/prowler-cloud/prowler/pull/9223)
- Update oraclecloud blockstorage service metadata to new format [(#9222)](https://github.com/prowler-cloud/prowler/pull/9222)
- Update oraclecloud audit service metadata to new format [(#9221)](https://github.com/prowler-cloud/prowler/pull/9221)
- GitHub App authentication inconsistency where `--github-app-key` and `--github-app-key-path` were incorrectly aliased to the same parameter, causing confusion between file paths and key content [(#8422)](https://github.com/prowler-cloud/prowler/pull/8422)
---

View File

@@ -242,6 +242,7 @@ class Provider(ABC):
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,
config_path=arguments.config_file,

View File

@@ -103,6 +103,7 @@ class GithubProvider(Provider):
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,
# Provider configuration
@@ -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.
github_app_key_content (str): GitHub App key content.
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 (legacy parameter).
github_app_id (int): GitHub App ID.
config_path (str): Path to the audit configuration file.
config_content (dict): Audit configuration content.
@@ -148,6 +150,7 @@ class GithubProvider(Provider):
oauth_app_token,
github_app_id,
github_app_key,
github_app_key_path,
github_app_key_content,
)
@@ -156,13 +159,17 @@ 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 or github_app_key_content):
self._auth_method = "GitHub App Token"
elif github_app_id and (
github_app_key or github_app_key_path or github_app_key_content
):
self._auth_method = "GitHub App Key and ID"
elif environ.get("GITHUB_PERSONAL_ACCESS_TOKEN", ""):
self._auth_method = "Environment Variable for Personal Access Token"
elif environ.get("GITHUB_OAUTH_APP_TOKEN", ""):
self._auth_method = "Environment Variable for OAuth App Token"
elif environ.get("GITHUB_APP_ID", "") and environ.get("GITHUB_APP_KEY", ""):
elif environ.get("GITHUB_APP_ID", "") and (
environ.get("GITHUB_APP_KEY", "") or environ.get("GITHUB_APP_KEY_PATH", "")
):
self._auth_method = "Environment Variables for GitHub App Key and ID"
self._identity = GithubProvider.setup_identity(self._session)
@@ -251,6 +258,7 @@ class GithubProvider(Provider):
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 +268,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.
github_app_key_content (str): GitHub App key content.
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 (legacy parameter).
Returns:
GithubSession: Authenticated session token for API requests.
"""
@@ -278,11 +287,35 @@ class GithubProvider(Provider):
elif oauth_app_token:
session_token = oauth_app_token
elif github_app_id and (github_app_key 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:
with open(github_app_key, "r") as rsa_key:
app_key = rsa_key.read()
if github_app_key_path:
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,13 +336,24 @@ 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", "")
app_key = format_rsa_key(environ.get("GITHUB_APP_KEY", ""))
if app_id and app_key:
pass
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)
else:
raise GithubEnvironmentVariableError(
file=os.path.basename(__file__),
message="GITHUB_APP_KEY must contain RSA key content (starting with -----BEGIN). Use GITHUB_APP_KEY_PATH for file paths.",
)
if not session_token and not (app_id and app_key):
raise GithubEnvironmentVariableError(
@@ -520,6 +564,7 @@ class GithubProvider(Provider):
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,
raise_on_exception: bool = True,
@@ -532,8 +577,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.
github_app_key_content (str): GitHub App key content.
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 (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.
@@ -553,7 +599,7 @@ class GithubProvider(Provider):
Examples:
>>> GithubProvider.test_connection(personal_access_token="ghp_xxxxxxxxxxxxxxxx")
Connection(is_connected=True)
>>> GithubProvider.test_connection(github_app_id=12345, github_app_key="/path/to/key.pem")
>>> GithubProvider.test_connection(github_app_id=12345, github_app_key_content="/path/to/key.pem")
Connection(is_connected=True)
>>> GithubProvider.test_connection(provider_id="my-org")
Connection(is_connected=True)
@@ -565,6 +611,7 @@ class GithubProvider(Provider):
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,
)

View File

@@ -31,12 +31,18 @@ def init_parser(self):
)
github_auth_subparser.add_argument(
"--github-app-key",
"--github-app-key-path",
nargs="?",
help="GitHub App Key Path to log in against GitHub",
help="GitHub App Key content (PEM format) to log in against GitHub",
default=None,
metavar="GITHUB_APP_KEY",
)
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",
)
github_scoping_subparser = github_parser.add_argument_group("Scan Scoping")
github_scoping_subparser.add_argument(
@@ -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, ""

View File

@@ -39,6 +39,7 @@ class TestGitHubProvider:
oauth_app_token = None
github_app_id = None
github_app_key = None
github_app_key_path = None
fixer_config = load_and_validate_config_file(
"github", default_fixer_config_file_path
)
@@ -62,6 +63,7 @@ class TestGitHubProvider:
oauth_app_token,
github_app_id,
github_app_key,
github_app_key_path,
)
assert provider._type == "github"
@@ -80,7 +82,7 @@ class TestGitHubProvider:
personal_access_token = None
oauth_app_token = OAUTH_TOKEN
github_app_id = None
github_app_key = None
github_app_key_path = None
fixer_config = load_and_validate_config_file(
"github", default_fixer_config_file_path
)
@@ -103,7 +105,7 @@ class TestGitHubProvider:
personal_access_token,
oauth_app_token,
github_app_id,
github_app_key,
github_app_key_path,
)
assert provider._type == "github"
@@ -118,11 +120,13 @@ class TestGitHubProvider:
}
assert provider._fixer_config == fixer_config
def test_github_provider_App(self):
def test_github_provider_App_with_key_path(self):
personal_access_token = None
oauth_app_token = None
github_app_id = APP_ID
github_app_key = APP_KEY
github_app_key = None
github_app_key_path = APP_KEY
github_app_key_content = None
fixer_config = load_and_validate_config_file(
"github", default_fixer_config_file_path
)
@@ -144,8 +148,10 @@ class TestGitHubProvider:
provider = GithubProvider(
personal_access_token,
oauth_app_token,
github_app_id,
github_app_key,
github_app_key_path,
github_app_key_content,
github_app_id,
)
assert provider._type == "github"
@@ -158,6 +164,104 @@ class TestGitHubProvider:
}
assert provider._fixer_config == fixer_config
def test_github_provider_App_with_key_content(self):
personal_access_token = None
oauth_app_token = None
github_app_id = APP_ID
github_app_key = (
"-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----"
)
github_app_key_path = None
github_app_key_content = None
fixer_config = load_and_validate_config_file(
"github", default_fixer_config_file_path
)
with (
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_session",
return_value=GithubSession(token="", id=APP_ID, key=github_app_key),
),
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_identity",
return_value=GithubAppIdentityInfo(
app_id=APP_ID,
app_name=APP_NAME,
installations=["test-org"],
),
),
):
provider = GithubProvider(
personal_access_token,
oauth_app_token,
github_app_key,
github_app_key_path,
github_app_key_content,
github_app_id,
)
assert provider._type == "github"
assert provider.session == GithubSession(
token="", id=APP_ID, key=github_app_key
)
assert provider.identity == GithubAppIdentityInfo(
app_id=APP_ID, app_name=APP_NAME, installations=["test-org"]
)
assert provider._audit_config == {
"inactive_not_archived_days_threshold": 180,
}
assert provider._fixer_config == fixer_config
def test_github_provider_App_with_legacy_key_content(self):
personal_access_token = None
oauth_app_token = None
github_app_id = APP_ID
github_app_key = None
github_app_key_path = None
github_app_key_content = (
"-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----"
)
fixer_config = load_and_validate_config_file(
"github", default_fixer_config_file_path
)
with (
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_session",
return_value=GithubSession(
token="", id=APP_ID, key=github_app_key_content
),
),
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_identity",
return_value=GithubAppIdentityInfo(
app_id=APP_ID,
app_name=APP_NAME,
installations=["test-org"],
),
),
):
provider = GithubProvider(
personal_access_token,
oauth_app_token,
github_app_key,
github_app_key_path,
github_app_key_content,
github_app_id,
)
assert provider._type == "github"
assert provider.session == GithubSession(
token="", id=APP_ID, key=github_app_key_content
)
assert provider.identity == GithubAppIdentityInfo(
app_id=APP_ID, app_name=APP_NAME, installations=["test-org"]
)
assert provider._audit_config == {
"inactive_not_archived_days_threshold": 180,
}
assert provider._fixer_config == fixer_config
def test_test_connection_with_personal_access_token_success(self):
"""Test successful connection with personal access token."""
with (
@@ -202,8 +306,8 @@ class TestGitHubProvider:
assert connection.is_connected is True
assert connection.error is None
def test_test_connection_with_github_app_success(self):
"""Test successful connection with GitHub App credentials."""
def test_test_connection_with_github_app_key_path_success(self):
"""Test successful connection with GitHub App key path."""
with (
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_session",
@@ -217,7 +321,57 @@ class TestGitHubProvider:
),
):
connection = GithubProvider.test_connection(
github_app_id=APP_ID, github_app_key=APP_KEY
github_app_id=APP_ID, github_app_key_path=APP_KEY
)
assert isinstance(connection, Connection)
assert connection.is_connected is True
assert connection.error is None
def test_test_connection_with_github_app_key_content_success(self):
"""Test successful connection with GitHub App key content."""
key_content = (
"-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----"
)
with (
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_session",
return_value=GithubSession(token="", id=APP_ID, key=key_content),
),
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_identity",
return_value=GithubAppIdentityInfo(
app_id=APP_ID, app_name=APP_NAME, installations=["test-org"]
),
),
):
connection = GithubProvider.test_connection(
github_app_id=APP_ID, github_app_key=key_content
)
assert isinstance(connection, Connection)
assert connection.is_connected is True
assert connection.error is None
def test_test_connection_with_github_app_legacy_key_content_success(self):
"""Test successful connection with GitHub App legacy key content."""
key_content = (
"-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----"
)
with (
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_session",
return_value=GithubSession(token="", id=APP_ID, key=key_content),
),
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_identity",
return_value=GithubAppIdentityInfo(
app_id=APP_ID, app_name=APP_NAME, installations=["test-org"]
),
),
):
connection = GithubProvider.test_connection(
github_app_id=APP_ID, github_app_key_content=key_content
)
assert isinstance(connection, Connection)
@@ -279,7 +433,7 @@ class TestGitHubProvider:
):
with pytest.raises(GithubInvalidCredentialsError):
GithubProvider.test_connection(
github_app_id=APP_ID, github_app_key="invalid-key"
github_app_id=APP_ID, github_app_key_path="invalid-key"
)
def test_test_connection_with_invalid_app_credentials_no_raise(self):
@@ -298,7 +452,7 @@ class TestGitHubProvider:
):
connection = GithubProvider.test_connection(
github_app_id=APP_ID,
github_app_key="invalid-key",
github_app_key_path="invalid-key",
raise_on_exception=False,
)
@@ -306,6 +460,104 @@ class TestGitHubProvider:
assert connection.is_connected is False
assert isinstance(connection.error, GithubInvalidCredentialsError)
def test_test_connection_github_app_key_path_with_content_raises_exception(self):
"""Test connection when github_app_key_path receives key content instead of file path."""
key_content = (
"-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----"
)
with (
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_session",
side_effect=GithubEnvironmentVariableError(
file="github_provider.py",
message="--github-app-key-path expects a file path, not key content. Use --github-app-key for key content instead.",
),
),
patch("prowler.providers.github.github_provider.logger") as mock_logger,
):
with pytest.raises(GithubEnvironmentVariableError) as exc_info:
GithubProvider.test_connection(
github_app_id=APP_ID, github_app_key_path=key_content
)
assert "--github-app-key-path expects a file path" in str(exc_info.value)
mock_logger.critical.assert_called_once()
def test_test_connection_github_app_key_with_file_path_raises_exception(self):
"""Test connection when github_app_key receives file path instead of key content."""
file_path = "/path/to/key.pem"
with (
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_session",
side_effect=GithubEnvironmentVariableError(
file="github_provider.py",
message="--github-app-key expects key content, not a file path. Use --github-app-key-path for file paths instead.",
),
),
patch("prowler.providers.github.github_provider.logger") as mock_logger,
):
with pytest.raises(GithubEnvironmentVariableError) as exc_info:
GithubProvider.test_connection(
github_app_id=APP_ID, github_app_key=file_path
)
assert "--github-app-key expects key content" in str(exc_info.value)
mock_logger.critical.assert_called_once()
def test_test_connection_github_app_key_path_with_content_no_raise(self):
"""Test connection when github_app_key_path receives key content without raising exception."""
key_content = (
"-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----"
)
with (
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_session",
side_effect=GithubEnvironmentVariableError(
file="github_provider.py",
message="--github-app-key-path expects a file path, not key content. Use --github-app-key for key content instead.",
),
),
patch("prowler.providers.github.github_provider.logger") as mock_logger,
):
connection = GithubProvider.test_connection(
github_app_id=APP_ID,
github_app_key_path=key_content,
raise_on_exception=False,
)
assert isinstance(connection, Connection)
assert connection.is_connected is False
assert isinstance(connection.error, GithubEnvironmentVariableError)
assert "--github-app-key-path expects a file path" in str(connection.error)
mock_logger.critical.assert_called_once()
def test_test_connection_github_app_key_with_file_path_no_raise(self):
"""Test connection when github_app_key receives file path without raising exception."""
file_path = "/path/to/key.pem"
with (
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_session",
side_effect=GithubEnvironmentVariableError(
file="github_provider.py",
message="--github-app-key expects key content, not a file path. Use --github-app-key-path for file paths instead.",
),
),
patch("prowler.providers.github.github_provider.logger") as mock_logger,
):
connection = GithubProvider.test_connection(
github_app_id=APP_ID, github_app_key=file_path, raise_on_exception=False
)
assert isinstance(connection, Connection)
assert connection.is_connected is False
assert isinstance(connection.error, GithubEnvironmentVariableError)
assert "--github-app-key expects key content" in str(connection.error)
mock_logger.critical.assert_called_once()
def test_test_connection_setup_session_error_raises_exception(self):
"""Test connection when setup_session raises an exception."""
with (
@@ -538,6 +790,103 @@ class TestGitHubProvider:
assert connection.is_connected is False
assert isinstance(connection.error, GithubInvalidProviderIdError)
def test_setup_session_with_github_app_key_path_env_var(self):
"""Test setup_session with GITHUB_APP_KEY_PATH environment variable."""
import os
import tempfile
key_content = (
"-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----"
)
with tempfile.NamedTemporaryFile(mode="w", suffix=".pem", delete=False) as f:
f.write(key_content)
temp_path = f.name
try:
with patch.dict(
os.environ,
{"GITHUB_APP_ID": str(APP_ID), "GITHUB_APP_KEY_PATH": temp_path},
):
# Clear other env vars that might interfere
for key in [
"GITHUB_PERSONAL_ACCESS_TOKEN",
"GITHUB_OAUTH_APP_TOKEN",
"GITHUB_APP_KEY",
]:
if key in os.environ:
del os.environ[key]
session = GithubProvider.setup_session()
assert session.id == str(APP_ID)
assert session.key == key_content
assert session.token == ""
finally:
os.unlink(temp_path)
def test_setup_session_with_github_app_key_env_var_content(self):
"""Test setup_session with GITHUB_APP_KEY environment variable containing key content."""
import os
key_content = (
"-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----"
)
with patch.dict(
os.environ, {"GITHUB_APP_ID": str(APP_ID), "GITHUB_APP_KEY": key_content}
):
# Clear other env vars that might interfere
for key in [
"GITHUB_PERSONAL_ACCESS_TOKEN",
"GITHUB_OAUTH_APP_TOKEN",
"GITHUB_APP_KEY_PATH",
]:
if key in os.environ:
del os.environ[key]
session = GithubProvider.setup_session()
assert session.id == str(APP_ID)
assert session.key == key_content
assert session.token == ""
def test_setup_session_with_github_app_key_env_var_file_path(self):
"""Test setup_session with GITHUB_APP_KEY environment variable containing file path should raise error."""
import os
import tempfile
key_content = (
"-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----"
)
with tempfile.NamedTemporaryFile(mode="w", suffix=".pem", delete=False) as f:
f.write(key_content)
temp_path = f.name
try:
with patch.dict(
os.environ, {"GITHUB_APP_ID": str(APP_ID), "GITHUB_APP_KEY": temp_path}
):
# Clear other env vars that might interfere
for key in [
"GITHUB_PERSONAL_ACCESS_TOKEN",
"GITHUB_OAUTH_APP_TOKEN",
"GITHUB_APP_KEY_PATH",
]:
if key in os.environ:
del os.environ[key]
with pytest.raises(GithubSetUpSessionError) as exc_info:
GithubProvider.setup_session()
assert "GITHUB_APP_KEY must contain RSA key content" in str(
exc_info.value
)
assert "Use GITHUB_APP_KEY_PATH for file paths" in str(exc_info.value)
finally:
os.unlink(temp_path)
def test_validate_provider_id_with_valid_user(self):
"""Test validate_provider_id with valid user (matches authenticated user)."""
mock_session = GithubSession(token=PAT_TOKEN, id="", key="")

View File

@@ -61,17 +61,23 @@ 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
auth_args = [call[0][0] for call in calls]
auth_args = []
for call in calls:
# Handle both single arguments and aliases
if len(call[0]) > 1:
auth_args.extend(call[0])
else:
auth_args.append(call[0][0])
assert "--personal-access-token" in auth_args
assert "--oauth-app-token" in auth_args
assert "--github-app-id" in auth_args
# Check for either form of the github app key argument
assert any("--github-app-key" in arg for arg 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"""
@@ -303,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 == ""