Skip to content

Commit 883b7e7

Browse files
committed
chore(github): improve docs and error handling
1 parent 8b84823 commit 883b7e7

File tree

7 files changed

+417
-85
lines changed

7 files changed

+417
-85
lines changed

prowler/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
55
## [v5.10.0] (Prowler UNRELEASED)
66

77
### Added
8-
- GitHub repository scoping behavior to prevent fallback organization scans when using `--repository` flag with invalid repository formats [(#8329)](https://github.com/prowler-cloud/prowler/pull/8329)
8+
- GitHub repository and organization scoping support with `--repository` and `--organization` flags [(#8329)](https://github.com/prowler-cloud/prowler/pull/8329)
99

1010
### Changed
1111

prowler/providers/github/github_provider.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ def __init__(
110110
fixer_config: dict = {},
111111
mutelist_path: str = None,
112112
mutelist_content: dict = None,
113-
# Repository scoping
114113
repositories: list = None,
115114
organizations: list = None,
116115
):
@@ -319,7 +318,7 @@ def setup_session(
319318

320319
except Exception as error:
321320
logger.critical(
322-
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
321+
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
323322
)
324323
raise GithubSetUpSessionError(
325324
original_exception=error,
@@ -368,7 +367,7 @@ def setup_identity(
368367

369368
except Exception as error:
370369
logger.critical(
371-
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
370+
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
372371
)
373372
raise GithubSetUpIdentityError(
374373
original_exception=error,

prowler/providers/github/services/organization/organization_service.py

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Optional
22

3+
import github
34
from pydantic.v1 import BaseModel
45

56
from prowler.lib.logger import logger
@@ -12,6 +13,25 @@ def __init__(self, provider):
1213
self.organizations = self._list_organizations()
1314

1415
def _list_organizations(self):
16+
"""
17+
List organizations based on provider scoping configuration.
18+
19+
Scoping behavior:
20+
- No scoping: Returns all organizations for authenticated user
21+
- Organization scoping: Returns only specified organizations
22+
Example: --organization org1 org2
23+
- Repository + Organization scoping: Returns specified organizations + repository owners
24+
Example: --repository owner1/repo1 --organization org2
25+
- Repository only: Returns empty (no organization checks)
26+
Example: --repository owner1/repo1
27+
28+
Returns:
29+
dict: Dictionary of organization ID to Org objects
30+
31+
Raises:
32+
github.GithubException: When GitHub API access fails
33+
github.RateLimitExceededException: When API rate limits are exceeded
34+
"""
1535
logger.info("Organization - Listing Organizations...")
1636
organizations = {}
1737
org_names_to_check = set()
@@ -39,7 +59,7 @@ def _list_organizations(self):
3959
try:
4060
org = client.get_organization(org_name)
4161
self._process_organization(org, organizations)
42-
except Exception as org_error:
62+
except github.GithubException as org_error:
4363
# If organization fails, try as a user (personal account)
4464
if "404" in str(org_error):
4565
logger.info(
@@ -56,22 +76,51 @@ def _list_organizations(self):
5676
logger.info(
5777
f"Added user '{user.login}' as organization for checks"
5878
)
79+
except github.GithubException as user_error:
80+
if "404" in str(user_error):
81+
logger.warning(
82+
f"'{org_name}' not found as organization or user"
83+
)
84+
elif "403" in str(user_error):
85+
logger.warning(
86+
f"Access denied to '{org_name}' - insufficient permissions"
87+
)
88+
else:
89+
logger.warning(
90+
f"GitHub API error accessing '{org_name}' as user: {user_error}"
91+
)
5992
except Exception as user_error:
60-
logger.warning(
61-
f"'{org_name}' not accessible as organization or user: {user_error}"
93+
logger.error(
94+
f"{user_error.__class__.__name__}[{user_error.__traceback__.tb_lineno}]: {user_error}"
6295
)
63-
else:
96+
elif "403" in str(org_error):
6497
logger.warning(
65-
f"Organization '{org_name}' not accessible: {org_error}"
98+
f"Access denied to organization '{org_name}' - insufficient permissions"
6699
)
100+
else:
101+
logger.error(
102+
f"GitHub API error accessing organization '{org_name}': {org_error}"
103+
)
104+
except github.RateLimitExceededException as error:
105+
logger.error(
106+
f"Rate limit exceeded while processing organization '{org_name}': {error}"
107+
)
108+
raise # Re-raise rate limit errors as they need special handling
67109
except Exception as error:
68-
logger.warning(f"Error accessing '{org_name}': {error}")
110+
logger.error(
111+
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
112+
)
69113
elif not self.provider.repositories:
70114
# Default behavior: get all organizations the user is a member of
71115
# Only when no repositories are specified
72116
for org in client.get_user().get_orgs():
73117
self._process_organization(org, organizations)
74118

119+
except github.RateLimitExceededException as error:
120+
logger.error(f"GitHub API rate limit exceeded: {error}")
121+
raise # Re-raise rate limit errors as they need special handling
122+
except github.GithubException as error:
123+
logger.error(f"GitHub API error while listing organizations: {error}")
75124
except Exception as error:
76125
logger.error(
77126
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"

prowler/providers/github/services/repository/repository_service.py

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
22
from typing import Optional
33

4+
import github
45
from pydantic.v1 import BaseModel
56

67
from prowler.lib.logger import logger
@@ -25,7 +26,50 @@ def _file_exists(self, repo, filename):
2526
)
2627
return None
2728

29+
def _validate_repository_format(self, repo_name: str) -> bool:
30+
"""
31+
Validate repository name format.
32+
33+
Args:
34+
repo_name: Repository name to validate
35+
36+
Returns:
37+
bool: True if format is valid, False otherwise
38+
"""
39+
if not repo_name or "/" not in repo_name:
40+
return False
41+
42+
parts = repo_name.split("/")
43+
if len(parts) != 2:
44+
return False
45+
46+
owner, repo = parts
47+
# Ensure both owner and repo names are non-empty
48+
if not owner.strip() or not repo.strip():
49+
return False
50+
51+
return True
52+
2853
def _list_repositories(self):
54+
"""
55+
List repositories based on provider scoping configuration.
56+
57+
Scoping behavior:
58+
- No scoping: Returns all accessible repositories for authenticated user
59+
- Repository scoping: Returns only specified repositories
60+
Example: --repository owner1/repo1 owner2/repo2
61+
- Organization scoping: Returns all repositories from specified organizations
62+
Example: --organization org1 org2
63+
- Combined scoping: Returns specified repositories + all repos from organizations
64+
Example: --repository owner1/repo1 --organization org2
65+
66+
Returns:
67+
dict: Dictionary of repository ID to Repo objects
68+
69+
Raises:
70+
github.GithubException: When GitHub API access fails
71+
github.RateLimitExceededException: When API rate limits are exceeded
72+
"""
2973
logger.info("Repository - Listing Repositories...")
3074
repos = {}
3175
try:
@@ -36,17 +80,35 @@ def _list_repositories(self):
3680
f"Filtering for specific repositories: {self.provider.repositories}"
3781
)
3882
for repo_name in self.provider.repositories:
39-
if "/" not in repo_name:
83+
if not self._validate_repository_format(repo_name):
4084
logger.warning(
4185
f"Repository name '{repo_name}' should be in 'owner/repo-name' format. Skipping."
4286
)
4387
continue
4488
try:
4589
repo = client.get_repo(repo_name)
4690
self._process_repository(repo, repos)
91+
except github.GithubException as error:
92+
if "404" in str(error):
93+
logger.warning(
94+
f"Repository '{repo_name}' not found or not accessible"
95+
)
96+
elif "403" in str(error):
97+
logger.warning(
98+
f"Access denied to repository '{repo_name}' - insufficient permissions"
99+
)
100+
else:
101+
logger.error(
102+
f"GitHub API error for repository '{repo_name}': {error}"
103+
)
104+
except github.RateLimitExceededException as error:
105+
logger.error(
106+
f"Rate limit exceeded while accessing repository '{repo_name}': {error}"
107+
)
108+
raise # Re-raise rate limit errors as they need special handling
47109
except Exception as error:
48-
logger.warning(
49-
f"Repository '{repo_name}' not accessible or not found: {error}"
110+
logger.error(
111+
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
50112
)
51113

52114
if self.provider.organizations:
@@ -59,7 +121,7 @@ def _list_repositories(self):
59121
org = client.get_organization(org_name)
60122
for repo in org.get_repos():
61123
self._process_repository(repo, repos)
62-
except Exception as org_error:
124+
except github.GithubException as org_error:
63125
# If organization fails, try as a user
64126
if "404" in str(org_error):
65127
logger.info(
@@ -69,19 +131,48 @@ def _list_repositories(self):
69131
user = client.get_user(org_name)
70132
for repo in user.get_repos():
71133
self._process_repository(repo, repos)
134+
except github.GithubException as user_error:
135+
if "404" in str(user_error):
136+
logger.warning(
137+
f"'{org_name}' not found as organization or user"
138+
)
139+
elif "403" in str(user_error):
140+
logger.warning(
141+
f"Access denied to '{org_name}' - insufficient permissions"
142+
)
143+
else:
144+
logger.warning(
145+
f"GitHub API error accessing '{org_name}' as user: {user_error}"
146+
)
72147
except Exception as user_error:
73-
logger.warning(
74-
f"'{org_name}' not accessible as organization or user: {user_error}"
148+
logger.error(
149+
f"{user_error.__class__.__name__}[{user_error.__traceback__.tb_lineno}]: {user_error}"
75150
)
76-
else:
151+
elif "403" in str(org_error):
77152
logger.warning(
78-
f"Organization '{org_name}' not accessible: {org_error}"
153+
f"Access denied to organization '{org_name}' - insufficient permissions"
79154
)
155+
else:
156+
logger.error(
157+
f"GitHub API error accessing organization '{org_name}': {org_error}"
158+
)
159+
except github.RateLimitExceededException as error:
160+
logger.error(
161+
f"Rate limit exceeded while processing organization '{org_name}': {error}"
162+
)
163+
raise # Re-raise rate limit errors as they need special handling
80164
except Exception as error:
81-
logger.warning(f"Error accessing '{org_name}': {error}")
165+
logger.error(
166+
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
167+
)
82168
else:
83169
for repo in client.get_user().get_repos():
84170
self._process_repository(repo, repos)
171+
except github.RateLimitExceededException as error:
172+
logger.error(f"GitHub API rate limit exceeded: {error}")
173+
raise # Re-raise rate limit errors as they need special handling
174+
except github.GithubException as error:
175+
logger.error(f"GitHub API error while listing repositories: {error}")
85176
except Exception as error:
86177
logger.error(
87178
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"

tests/providers/github/github_provider_test.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,3 +701,35 @@ def test_provider_scoping_integration(self):
701701

702702
assert provider_none.repositories == []
703703
assert provider_none.organizations == []
704+
705+
def test_provider_configuration_validation(self):
706+
"""Test that provider validates configuration parameters"""
707+
with (
708+
patch(
709+
"prowler.providers.github.github_provider.GithubProvider.setup_session",
710+
return_value=self.mock_session,
711+
),
712+
patch(
713+
"prowler.providers.github.github_provider.GithubProvider.setup_identity",
714+
return_value=self.mock_identity,
715+
),
716+
):
717+
# Test invalid repositories type
718+
try:
719+
GithubProvider(
720+
personal_access_token=PAT_TOKEN,
721+
repositories="not-a-list", # Should be a list
722+
)
723+
assert False, "Should have raised ValueError"
724+
except ValueError as e:
725+
assert "repositories must be a list" in str(e)
726+
727+
# Test invalid organizations type
728+
try:
729+
GithubProvider(
730+
personal_access_token=PAT_TOKEN,
731+
organizations="not-a-list", # Should be a list
732+
)
733+
assert False, "Should have raised ValueError"
734+
except ValueError as e:
735+
assert "organizations must be a list" in str(e)

0 commit comments

Comments
 (0)