fix(github): combine --repository and --organization flags for scan scoping (#10005)

Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
This commit is contained in:
Prowler Bot
2026-02-10 14:44:01 +01:00
committed by GitHub
parent b8da7c9619
commit 6d94f0fcc3
6 changed files with 189 additions and 26 deletions

View File

@@ -38,6 +38,7 @@ GitHub has deprecated Personal Access Tokens (classic) in favor of fine-grained
4. **Configure Token Settings** 4. **Configure Token Settings**
- **Token name**: Give your token a descriptive name (e.g., "Prowler Security Scanner") - **Token name**: Give your token a descriptive name (e.g., "Prowler Security Scanner")
- **Resource owner**: Select the account that owns the resources to scan — either a personal account or a specific organization
- **Expiration**: Set an appropriate expiration date (recommended: 90 days or less) - **Expiration**: Set an appropriate expiration date (recommended: 90 days or less)
- **Repository access**: Choose "All repositories" or "Only select repositories" based on your needs - **Repository access**: Choose "All repositories" or "Only select repositories" based on your needs
@@ -56,11 +57,11 @@ GitHub has deprecated Personal Access Tokens (classic) in favor of fine-grained
- **Metadata**: Read-only access - **Metadata**: Read-only access
- **Pull requests**: Read-only access - **Pull requests**: Read-only access
- **Organization permissions:** - **Organization permissions** (available when an organization is selected as Resource Owner):
- **Administration**: Read-only access - **Administration**: Read-only access
- **Members**: Read-only access - **Members**: Read-only access
- **Account permissions:** - **Account permissions** (available when a personal account is selected as Resource Owner):
- **Email addresses**: Read-only access - **Email addresses**: Read-only access
6. **Copy and Store the Token** 6. **Copy and Store the Token**

View File

@@ -54,7 +54,7 @@ title: 'Getting Started with GitHub'
</Tabs> </Tabs>
## Prowler CLI ## Prowler CLI
### Automatic Login Method Detection ### Authentication
If no login method is explicitly provided, Prowler will automatically attempt to authenticate using environment variables in the following order of precedence: If no login method is explicitly provided, Prowler will automatically attempt to authenticate using environment variables in the following order of precedence:
@@ -68,15 +68,15 @@ Ensure the corresponding environment variables are set up before running Prowler
</Note> </Note>
For more details on how to set up authentication with GitHub, see [Authentication > GitHub](/user-guide/providers/github/authentication). For more details on how to set up authentication with GitHub, see [Authentication > GitHub](/user-guide/providers/github/authentication).
### Personal Access Token (PAT) #### Personal Access Token (PAT)
Use this method by providing your personal access token directly. Use this method by providing a personal access token directly.
```console ```console
prowler github --personal-access-token pat prowler github --personal-access-token pat
``` ```
### OAuth App Token #### OAuth App Token
Authenticate using an OAuth app token. Authenticate using an OAuth app token.
@@ -84,9 +84,62 @@ Authenticate using an OAuth app token.
prowler github --oauth-app-token oauth_token prowler github --oauth-app-token oauth_token
``` ```
### GitHub App Credentials #### GitHub App Credentials
Use GitHub App credentials by specifying the App ID and the private key path. Use GitHub App credentials by specifying the App ID and the private key path.
```console ```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 app_key_path
``` ```
### Scan Scoping
By default, Prowler scans all repositories accessible to the authenticated user or organization. To limit the scan to specific repositories or organizations, use the following flags.
#### Scanning Specific Repositories
To restrict the scan to one or more repositories, use the `--repository` flag followed by the repository name(s) in `owner/repo-name` format:
```console
prowler github --repository owner/repo-name
```
To scan multiple repositories, specify them as space-separated arguments:
```console
prowler github --repository owner/repo-name-1 owner/repo-name-2
```
#### Scanning Specific Organizations
To restrict the scan to one or more organizations or user accounts, use the `--organization` flag:
```console
prowler github --organization my-organization
```
To scan multiple organizations, specify them as space-separated arguments:
```console
prowler github --organization org-1 org-2
```
#### Scanning Specific Repositories Within an Organization
To scan specific repositories within an organization, combine the `--organization` and `--repository` flags. The `--organization` flag qualifies unqualified repository names automatically:
```console
prowler github --organization my-organization --repository my-repo
```
This scans only `my-organization/my-repo`. Fully qualified repository names (`owner/repo-name`) are also supported alongside `--organization`:
```console
prowler github --organization my-org --repository my-repo other-owner/other-repo
```
In this case, `my-repo` is qualified as `my-org/my-repo`, while `other-owner/other-repo` is used as-is.
<Note>
The `--repository` and `--organization` flags can be combined with any authentication method.
</Note>

View File

@@ -2,6 +2,14 @@
All notable changes to the **Prowler SDK** are documented in this file. All notable changes to the **Prowler SDK** are documented in this file.
## [5.18.2] (Prowler UNRELEASED)
### 🐞 Fixed
- `--repository` and `--organization` flags combined interaction in GitHub provider, qualifying unqualified repository names with organization [(#10001)](https://github.com/prowler-cloud/prowler/pull/10001)
---
## [5.18.0] (Prowler v5.18.0) ## [5.18.0] (Prowler v5.18.0)
### 🚀 Added ### 🚀 Added

View File

@@ -121,15 +121,22 @@ class Repository(GithubService):
) )
): ):
if self.provider.repositories: if self.provider.repositories:
logger.info( qualified_repos = []
f"Filtering for specific repositories: {self.provider.repositories}"
)
for repo_name in self.provider.repositories: for repo_name in self.provider.repositories:
if not self._validate_repository_format(repo_name): if self._validate_repository_format(repo_name):
qualified_repos.append(repo_name)
elif self.provider.organizations:
for org_name in self.provider.organizations:
qualified_repos.append(f"{org_name}/{repo_name}")
else:
logger.warning( logger.warning(
f"Repository name '{repo_name}' should be in 'owner/repo-name' format. Skipping." f"Repository name '{repo_name}' should be in 'owner/repo-name' format. Skipping."
) )
continue
logger.info(
f"Filtering for specific repositories: {qualified_repos}"
)
for repo_name in qualified_repos:
try: try:
repo = client.get_repo(repo_name) repo = client.get_repo(repo_name)
self._process_repository(repo, repos) self._process_repository(repo, repos)
@@ -138,7 +145,7 @@ class Repository(GithubService):
error, "accessing repository", repo_name error, "accessing repository", repo_name
) )
if self.provider.organizations: elif self.provider.organizations:
logger.info( logger.info(
f"Filtering for repositories in organizations: {self.provider.organizations}" f"Filtering for repositories in organizations: {self.provider.organizations}"
) )

View File

@@ -74,6 +74,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash
- One entry per PR (can link multiple PRs for related changes) - One entry per PR (can link multiple PRs for related changes)
- No period at the end - No period at the end
- Do NOT start with redundant verbs (section header already provides the action) - Do NOT start with redundant verbs (section header already provides the action)
- **CRITICAL: Preserve section order** — when adding a new section to the UNRELEASED block, insert it in the correct position relative to existing sections (Added → Changed → Deprecated → Removed → Fixed → Security). Never append a new section at the top or bottom without checking order
### Semantic Versioning Rules ### Semantic Versioning Rules
@@ -177,6 +178,13 @@ This maintains chronological order within each section (oldest at top, newest at
### Bad Entries ### Bad Entries
```markdown ```markdown
# BAD - Wrong section order (Fixed before Added)
### 🐞 Fixed
- Some bug fix [(#123)](...)
### 🚀 Added
- Some new feature [(#456)](...)
- Fixed bug. # Too vague, has period - Fixed bug. # Too vague, has period
- Added new feature for users # Missing PR link, redundant verb - Added new feature for users # Missing PR link, redundant verb
- Add search bar [(#123)] # Redundant verb (section already says "Added") - Add search bar [(#123)] # Redundant verb (section already says "Added")

View File

@@ -245,19 +245,61 @@ class Test_Repository_Scoping:
self.mock_repo2.get_branch.side_effect = Exception("404 Not Found") self.mock_repo2.get_branch.side_effect = Exception("404 Not Found")
self.mock_repo2.get_dependabot_alerts.side_effect = Exception("404 Not Found") self.mock_repo2.get_dependabot_alerts.side_effect = Exception("404 Not Found")
def test_combined_repository_and_organization_scoping(self): def test_qualified_repo_with_organization_skips_org_fetch(self):
"""Test that both repository and organization scoping can be used together""" """Test that a fully qualified repo with --organization does not fetch all org repos"""
provider = set_mocked_github_provider() provider = set_mocked_github_provider()
provider.repositories = ["owner1/repo1"] provider.repositories = ["owner1/repo1"]
provider.organizations = ["org2"] provider.organizations = ["org2"]
mock_client = MagicMock() mock_client = MagicMock()
# Repository lookup
mock_client.get_repo.return_value = self.mock_repo1 mock_client.get_repo.return_value = self.mock_repo1
# Organization lookup
mock_org = MagicMock() with patch(
mock_org.get_repos.return_value = [self.mock_repo2] "prowler.providers.github.services.repository.repository_service.GithubService.__init__"
mock_client.get_organization.return_value = mock_org ):
repository_service = Repository(provider)
repository_service.clients = [mock_client]
repository_service.provider = provider
repos = repository_service._list_repositories()
assert len(repos) == 1
assert 1 in repos
assert repos[1].name == "repo1"
mock_client.get_repo.assert_called_once_with("owner1/repo1")
mock_client.get_organization.assert_not_called()
def test_unqualified_repo_qualified_with_organization(self):
"""Test that an unqualified repo name is qualified with the organization"""
provider = set_mocked_github_provider()
provider.repositories = ["repo1"]
provider.organizations = ["owner1"]
mock_client = MagicMock()
mock_client.get_repo.return_value = self.mock_repo1
with patch(
"prowler.providers.github.services.repository.repository_service.GithubService.__init__"
):
repository_service = Repository(provider)
repository_service.clients = [mock_client]
repository_service.provider = provider
repos = repository_service._list_repositories()
assert len(repos) == 1
assert 1 in repos
assert repos[1].name == "repo1"
mock_client.get_repo.assert_called_once_with("owner1/repo1")
def test_unqualified_repo_qualified_with_multiple_organizations(self):
"""Test that an unqualified repo is qualified with each organization"""
provider = set_mocked_github_provider()
provider.repositories = ["repo1"]
provider.organizations = ["org1", "org2"]
mock_client = MagicMock()
mock_client.get_repo.side_effect = [self.mock_repo1, self.mock_repo2]
with patch( with patch(
"prowler.providers.github.services.repository.repository_service.GithubService.__init__" "prowler.providers.github.services.repository.repository_service.GithubService.__init__"
@@ -269,12 +311,56 @@ class Test_Repository_Scoping:
repos = repository_service._list_repositories() repos = repository_service._list_repositories()
assert len(repos) == 2 assert len(repos) == 2
assert 1 in repos mock_client.get_repo.assert_any_call("org1/repo1")
assert 2 in repos mock_client.get_repo.assert_any_call("org2/repo1")
assert repos[1].name == "repo1"
assert repos[2].name == "repo2" def test_unqualified_repo_without_organization_is_skipped(self):
mock_client.get_repo.assert_called_once_with("owner1/repo1") """Test that an unqualified repo without --organization is skipped with a warning"""
mock_client.get_organization.assert_called_once_with("org2") provider = set_mocked_github_provider()
provider.repositories = ["repo1"]
provider.organizations = []
mock_client = MagicMock()
with patch(
"prowler.providers.github.services.repository.repository_service.GithubService.__init__"
):
repository_service = Repository(provider)
repository_service.clients = [mock_client]
repository_service.provider = provider
with patch(
"prowler.providers.github.services.repository.repository_service.logger"
) as mock_logger:
repos = repository_service._list_repositories()
assert len(repos) == 0
mock_logger.warning.assert_called_with(
"Repository name 'repo1' should be in 'owner/repo-name' format. Skipping."
)
mock_client.get_repo.assert_not_called()
def test_mixed_qualified_and_unqualified_repos_with_organization(self):
"""Test mix of qualified and unqualified repos with --organization"""
provider = set_mocked_github_provider()
provider.repositories = ["repo1", "owner2/repo2"]
provider.organizations = ["org1"]
mock_client = MagicMock()
mock_client.get_repo.side_effect = [self.mock_repo1, self.mock_repo2]
with patch(
"prowler.providers.github.services.repository.repository_service.GithubService.__init__"
):
repository_service = Repository(provider)
repository_service.clients = [mock_client]
repository_service.provider = provider
repos = repository_service._list_repositories()
assert len(repos) == 2
mock_client.get_repo.assert_any_call("org1/repo1")
mock_client.get_repo.assert_any_call("owner2/repo2")
class Test_Repository_Validation: class Test_Repository_Validation: