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**
- **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)
- **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
- **Pull requests**: Read-only access
- **Organization permissions:**
- **Organization permissions** (available when an organization is selected as Resource Owner):
- **Administration**: 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
6. **Copy and Store the Token**

View File

@@ -54,7 +54,7 @@ title: 'Getting Started with GitHub'
</Tabs>
## 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:
@@ -68,15 +68,15 @@ Ensure the corresponding environment variables are set up before running Prowler
</Note>
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
prowler github --personal-access-token pat
```
### OAuth App Token
#### 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
```
### GitHub App Credentials
#### GitHub App Credentials
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
```
### 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.
## [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)
### 🚀 Added

View File

@@ -121,15 +121,22 @@ class Repository(GithubService):
)
):
if self.provider.repositories:
logger.info(
f"Filtering for specific repositories: {self.provider.repositories}"
)
qualified_repos = []
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(
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:
repo = client.get_repo(repo_name)
self._process_repository(repo, repos)
@@ -138,7 +145,7 @@ class Repository(GithubService):
error, "accessing repository", repo_name
)
if self.provider.organizations:
elif self.provider.organizations:
logger.info(
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)
- No period at the end
- 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
@@ -177,6 +178,13 @@ This maintains chronological order within each section (oldest at top, newest at
### Bad Entries
```markdown
# BAD - Wrong section order (Fixed before Added)
### 🐞 Fixed
- Some bug fix [(#123)](...)
### 🚀 Added
- Some new feature [(#456)](...)
- Fixed bug. # Too vague, has period
- Added new feature for users # Missing PR link, redundant verb
- 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_dependabot_alerts.side_effect = Exception("404 Not Found")
def test_combined_repository_and_organization_scoping(self):
"""Test that both repository and organization scoping can be used together"""
def test_qualified_repo_with_organization_skips_org_fetch(self):
"""Test that a fully qualified repo with --organization does not fetch all org repos"""
provider = set_mocked_github_provider()
provider.repositories = ["owner1/repo1"]
provider.organizations = ["org2"]
mock_client = MagicMock()
# Repository lookup
mock_client.get_repo.return_value = self.mock_repo1
# Organization lookup
mock_org = MagicMock()
mock_org.get_repos.return_value = [self.mock_repo2]
mock_client.get_organization.return_value = mock_org
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")
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(
"prowler.providers.github.services.repository.repository_service.GithubService.__init__"
@@ -269,12 +311,56 @@ class Test_Repository_Scoping:
repos = repository_service._list_repositories()
assert len(repos) == 2
assert 1 in repos
assert 2 in repos
assert repos[1].name == "repo1"
assert repos[2].name == "repo2"
mock_client.get_repo.assert_called_once_with("owner1/repo1")
mock_client.get_organization.assert_called_once_with("org2")
mock_client.get_repo.assert_any_call("org1/repo1")
mock_client.get_repo.assert_any_call("org2/repo1")
def test_unqualified_repo_without_organization_is_skipped(self):
"""Test that an unqualified repo without --organization is skipped with a warning"""
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: