From dde42b6a8416d73bb66f36af490245acf070cff9 Mon Sep 17 00:00:00 2001 From: Andoni Alonso <14891798+andoniaf@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:34:59 +0100 Subject: [PATCH] fix(github): combine --repository and --organization flags for scan scoping (#10001) --- .../providers/github/authentication.mdx | 5 +- .../github/getting-started-github.mdx | 63 +++++++++- prowler/CHANGELOG.md | 6 + .../services/repository/repository_service.py | 19 ++- skills/prowler-changelog/SKILL.md | 8 ++ .../repository/repository_service_test.py | 112 ++++++++++++++++-- 6 files changed, 187 insertions(+), 26 deletions(-) diff --git a/docs/user-guide/providers/github/authentication.mdx b/docs/user-guide/providers/github/authentication.mdx index d76a0a1cb5..de7ed163d6 100644 --- a/docs/user-guide/providers/github/authentication.mdx +++ b/docs/user-guide/providers/github/authentication.mdx @@ -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** diff --git a/docs/user-guide/providers/github/getting-started-github.mdx b/docs/user-guide/providers/github/getting-started-github.mdx index 41d472df61..a9f870269b 100644 --- a/docs/user-guide/providers/github/getting-started-github.mdx +++ b/docs/user-guide/providers/github/getting-started-github.mdx @@ -54,7 +54,7 @@ title: 'Getting Started with GitHub' ## 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 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. + + +The `--repository` and `--organization` flags can be combined with any authentication method. + diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index fcdd8e474b..73848c87a5 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -12,6 +12,12 @@ All notable changes to the **Prowler SDK** are documented in this file. - Update Azure Monitor service metadata to new format [(#9622)](https://github.com/prowler-cloud/prowler/pull/9622) +## [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) diff --git a/prowler/providers/github/services/repository/repository_service.py b/prowler/providers/github/services/repository/repository_service.py index 244de81e08..88570f0fe7 100644 --- a/prowler/providers/github/services/repository/repository_service.py +++ b/prowler/providers/github/services/repository/repository_service.py @@ -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}" ) diff --git a/skills/prowler-changelog/SKILL.md b/skills/prowler-changelog/SKILL.md index 85a7fa4ba0..94f119e2ba 100644 --- a/skills/prowler-changelog/SKILL.md +++ b/skills/prowler-changelog/SKILL.md @@ -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") diff --git a/tests/providers/github/services/repository/repository_service_test.py b/tests/providers/github/services/repository/repository_service_test.py index e005e3a05b..932bf2c766 100644 --- a/tests/providers/github/services/repository/repository_service_test.py +++ b/tests/providers/github/services/repository/repository_service_test.py @@ -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: