feat(repositoy): add new check repository_inactive_not_archived (#7786)

Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
This commit is contained in:
Andoni Alonso
2025-05-26 10:39:09 +02:00
committed by GitHub
parent 50bcd828e9
commit eaec683eb9
25 changed files with 407 additions and 9 deletions
@@ -59,7 +59,9 @@ class TestGitHubProvider:
account_id=ACCOUNT_ID,
account_url=ACCOUNT_URL,
)
assert provider._audit_config == {}
assert provider._audit_config == {
"inactive_not_archived_days_threshold": 180,
}
assert provider._fixer_config == fixer_config
def test_github_provider_OAuth(self):
@@ -99,7 +101,9 @@ class TestGitHubProvider:
account_id=ACCOUNT_ID,
account_url=ACCOUNT_URL,
)
assert provider._audit_config == {}
assert provider._audit_config == {
"inactive_not_archived_days_threshold": 180,
}
assert provider._fixer_config == fixer_config
def test_github_provider_App(self):
@@ -133,5 +137,7 @@ class TestGitHubProvider:
assert provider._type == "github"
assert provider.session == GithubSession(token="", id=APP_ID, key=APP_KEY)
assert provider.identity == GithubAppIdentityInfo(app_id=APP_ID)
assert provider._audit_config == {}
assert provider._audit_config == {
"inactive_not_archived_days_threshold": 180,
}
assert provider._fixer_config == fixer_config
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
@@ -39,6 +40,8 @@ class Test_repository_branch_delete_on_merge_enabled_test:
private=False,
securitymd=False,
delete_branch_on_merge=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -79,6 +82,8 @@ class Test_repository_branch_delete_on_merge_enabled_test:
private=False,
securitymd=True,
delete_branch_on_merge=True,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
@@ -31,15 +32,18 @@ class Test_repository_default_branch_deletion_disabled_test:
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
now = datetime.now(timezone.utc)
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
full_name="account-name/repo1",
default_branch=default_branch,
default_branch_deletion=True,
private=False,
securitymd=False,
archived=False,
pushed_at=now,
default_branch_deletion=True,
),
}
@@ -61,7 +65,7 @@ class Test_repository_default_branch_deletion_disabled_test:
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
assert result[0].resource_name == "repo1"
assert result[0].resource_name == repo_name
assert result[0].status == "FAIL"
assert (
result[0].status_extended
@@ -72,15 +76,18 @@ class Test_repository_default_branch_deletion_disabled_test:
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
now = datetime.now(timezone.utc)
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
full_name="account-name/repo1",
private=False,
default_branch=default_branch,
private=False,
archived=False,
pushed_at=now,
default_branch_deletion=False,
securitymd=True,
),
}
@@ -102,7 +109,7 @@ class Test_repository_default_branch_deletion_disabled_test:
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
assert result[0].resource_name == "repo1"
assert result[0].resource_name == repo_name
assert result[0].status == "PASS"
assert (
result[0].status_extended
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
@@ -40,6 +41,8 @@ class Test_repository_default_branch_disallows_force_push_test:
allow_force_pushes=True,
private=False,
securitymd=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -81,6 +84,8 @@ class Test_repository_default_branch_disallows_force_push_test:
default_branch=default_branch,
allow_force_pushes=False,
securitymd=True,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
@@ -40,6 +41,8 @@ class Test_repository_default_branch_protection_applies_to_admins_test:
private=False,
securitymd=False,
enforce_admins=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -81,6 +84,8 @@ class Test_repository_default_branch_protection_applies_to_admins_test:
default_branch=default_branch,
enforce_admins=True,
securitymd=True,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
@@ -40,6 +41,8 @@ class Test_repository_default_branch_protection_enabled_test:
private=False,
default_branch_protection=False,
securitymd=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -81,6 +84,8 @@ class Test_repository_default_branch_protection_enabled_test:
default_branch=default_branch,
default_branch_protection=True,
securitymd=True,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
@@ -41,6 +42,8 @@ class Test_repository_default_branch_requires_codeowners_review:
require_pull_request=False,
approval_count=0,
require_code_owner_reviews=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -83,6 +86,8 @@ class Test_repository_default_branch_requires_codeowners_review:
require_pull_request=False,
approval_count=0,
require_code_owner_reviews=True,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
@@ -38,6 +39,8 @@ class Test_repository_default_branch_requires_conversation_resolution_test:
full_name="account-name/repo1",
default_branch=default_branch,
conversation_resolution=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
private=False,
securitymd=False,
),
@@ -80,6 +83,8 @@ class Test_repository_default_branch_requires_conversation_resolution_test:
private=False,
default_branch=default_branch,
conversation_resolution=True,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=True,
),
}
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
@@ -40,6 +41,8 @@ class Test_repository_default_branch_requires_linear_history_test:
required_linear_history=False,
private=False,
securitymd=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -81,6 +84,8 @@ class Test_repository_default_branch_requires_linear_history_test:
default_branch=default_branch,
required_linear_history=True,
securitymd=True,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
@@ -41,6 +42,8 @@ class Test_repository_default_branch_requires_multiple_approvals:
securitymd=False,
require_pull_request=False,
approval_count=0,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -83,6 +86,8 @@ class Test_repository_default_branch_requires_multiple_approvals:
securitymd=False,
require_pull_request=True,
approval_count=0,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -125,6 +130,8 @@ class Test_repository_default_branch_requires_multiple_approvals:
securitymd=True,
require_pull_request=True,
approval_count=2,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
@@ -40,6 +41,8 @@ class Test_repository_default_branch_requires_signed_commits:
default_branch=default_branch,
require_signed_commits=False,
securitymd=True,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -81,6 +84,8 @@ class Test_repository_default_branch_requires_signed_commits:
default_branch=default_branch,
require_signed_commits=True,
securitymd=True,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
@@ -38,6 +39,8 @@ class Test_repository_default_branch_status_checks_required_test:
full_name="account-name/repo1",
default_branch=default_branch,
status_checks=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
private=False,
securitymd=False,
),
@@ -80,6 +83,8 @@ class Test_repository_default_branch_status_checks_required_test:
private=False,
default_branch=default_branch,
status_checks=True,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=True,
),
}
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
@@ -37,6 +38,8 @@ class Test_repository_dependency_scanning_enabled:
full_name="account-name/repo1",
default_branch="main",
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=True,
require_pull_request=False,
approval_count=0,
@@ -80,6 +83,8 @@ class Test_repository_dependency_scanning_enabled:
full_name="account-name/repo2",
default_branch="main",
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=True,
require_pull_request=False,
approval_count=0,
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
@@ -41,6 +42,8 @@ class Test_repository_has_codeowners_file:
require_pull_request=False,
approval_count=0,
codeowners_exists=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -83,6 +86,8 @@ class Test_repository_has_codeowners_file:
require_pull_request=False,
approval_count=0,
codeowners_exists=True,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -0,0 +1,206 @@
from datetime import datetime, timedelta, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
from tests.providers.github.github_fixtures import set_mocked_github_provider
class Test_repository_inactive_not_archived:
def test_no_repositories(self):
repository_client = mock.MagicMock
repository_client.repositories = {}
repository_client.audit_config = {}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived import (
repository_inactive_not_archived,
)
check = repository_inactive_not_archived()
result = check.execute()
assert len(result) == 0
def test_repository_active_not_archived(self):
repository_client = mock.MagicMock
repo_name = "test-repo"
default_branch = "main"
now = datetime.now(timezone.utc)
recent_activity = now - timedelta(days=30) # 30 days ago
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
full_name="account-name/test-repo",
private=False,
default_branch=default_branch,
archived=False,
pushed_at=recent_activity,
),
}
repository_client.audit_config = {}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived import (
repository_inactive_not_archived,
)
check = repository_inactive_not_archived()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
assert result[0].resource_name == repo_name
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Repository {repo_name} has been active within the last 180 days (30 days ago)."
)
def test_repository_inactive_not_archived(self):
repository_client = mock.MagicMock
repo_name = "test-repo"
default_branch = "main"
now = datetime.now(timezone.utc)
old_activity = now - timedelta(days=200) # 200 days ago
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
full_name="account-name/test-repo",
private=False,
default_branch=default_branch,
archived=False,
pushed_at=old_activity,
),
}
repository_client.audit_config = {}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived import (
repository_inactive_not_archived,
)
check = repository_inactive_not_archived()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
assert result[0].resource_name == repo_name
assert result[0].status == "FAIL"
assert "has been inactive for 200 days" in result[0].status_extended
assert "and is not archived" in result[0].status_extended
def test_repository_inactive_but_archived(self):
repository_client = mock.MagicMock
repo_name = "test-repo"
default_branch = "main"
now = datetime.now(timezone.utc)
old_activity = now - timedelta(days=200) # 200 days ago
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
full_name="account-name/test-repo",
default_branch=default_branch,
private=False,
archived=True,
pushed_at=old_activity,
),
}
repository_client.audit_config = {}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived import (
repository_inactive_not_archived,
)
check = repository_inactive_not_archived()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
assert result[0].resource_name == repo_name
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Repository {repo_name} is properly archived."
)
def test_custom_days_threshold(self):
repository_client = mock.MagicMock
repo_name = "test-repo"
default_branch = "main"
now = datetime.now(timezone.utc)
old_activity = now - timedelta(days=50)
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
full_name="account-name/test-repo",
private=False,
default_branch=default_branch,
archived=False,
pushed_at=old_activity,
),
}
repository_client.audit_config = {"inactive_not_archived_days_threshold": 40}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_inactive_not_archived.repository_inactive_not_archived import (
repository_inactive_not_archived,
)
check = repository_inactive_not_archived()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
assert result[0].resource_name == repo_name
assert result[0].status == "FAIL"
assert "has been inactive for 50 days" in result[0].status_extended
assert "and is not archived" in result[0].status_extended
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
@@ -40,6 +41,8 @@ class Test_repository_public_has_securitymd_file_test:
securitymd=False,
require_pull_request=False,
approval_count=0,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -81,6 +84,8 @@ class Test_repository_public_has_securitymd_file_test:
securitymd=True,
require_pull_request=False,
approval_count=0,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
@@ -41,6 +42,8 @@ class Test_repository_secret_scanning_enabled:
require_pull_request=False,
approval_count=0,
secret_scanning_enabled=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -83,6 +86,8 @@ class Test_repository_secret_scanning_enabled:
require_pull_request=False,
approval_count=0,
secret_scanning_enabled=True,
archived=False,
pushed_at=datetime.now(timezone.utc),
),
}
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch
from prowler.providers.github.services.repository.repository_service import (
@@ -26,6 +27,9 @@ def mock_list_repositories(_):
codeowners_exists=True,
require_code_owner_reviews=True,
secret_scanning_enabled=True,
require_signed_commits=True,
archived=False,
pushed_at=datetime.now(timezone.utc),
enforce_admins=True,
delete_branch_on_merge=True,
conversation_resolution=True,
@@ -66,6 +70,9 @@ class Test_Repository_Service:
assert repository_service.repositories[1].codeowners_exists is True
assert repository_service.repositories[1].require_code_owner_reviews is True
assert repository_service.repositories[1].secret_scanning_enabled is True
assert repository_service.repositories[1].require_signed_commits is True
assert repository_service.repositories[1].archived is False
assert repository_service.repositories[1].pushed_at is not None
class Test_Repository_FileExists: