mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(github): support repository rulesets in default branch protection checks (#11723)
This commit is contained in:
committed by
GitHub
parent
aba43440ca
commit
2abcb05e22
@@ -26,6 +26,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- Compliance frameworks contributed by several external packages under the same provider are now merged instead of overwritten, so every entry-point directory a provider contributes is discovered [(#11578)](https://github.com/prowler-cloud/prowler/pull/11578)
|
||||
- Azure PostgreSQL flexible server collection no longer drops the remaining servers in a subscription when one server fails to collect; the `connection_throttle.enable` parameter (removed in PostgreSQL 16+) is treated as absent only when the Azure SDK reports it as not found, so unexpected lookup failures are not silently reported as throttling disabled [(#11595)](https://github.com/prowler-cloud/prowler/pull/11595)
|
||||
- Azure `keyvault_logging_enabled` now accepts Key Vault diagnostic settings that enable the explicit `AuditEvent` category, avoiding false failures when Azure returns category-based logs without category groups [(#11660)](https://github.com/prowler-cloud/prowler/pull/11660)
|
||||
- GitHub default branch protection checks now evaluate repository rulesets in addition to classic branch protection, avoiding false positives for repositories that enforce protection through rulesets [(#11723)](https://github.com/prowler-cloud/prowler/pull/11723)
|
||||
- Okta, Alibaba Cloud and OpenStack scan-config sections are now validated against a registered schema instead of being silently accepted, so their configurable thresholds (session/idle timeouts, retention days, image-sharing and secret-scanning settings) log a warning and fall back to the built-in default whenever a value is out of range [(#11725)](https://github.com/prowler-cloud/prowler/pull/11725)
|
||||
|
||||
---
|
||||
|
||||
+2
@@ -28,6 +28,8 @@ class repository_default_branch_deletion_disabled(Check):
|
||||
report.status_extended = (
|
||||
f"Repository {repo.name} does allow default branch deletion."
|
||||
)
|
||||
if repo.default_branch.branch_deletion_source == "ruleset_not_active":
|
||||
report.status_extended = f"Repository {repo.name} has default branch deletion disabled in a ruleset, but the ruleset is not active."
|
||||
|
||||
if not repo.default_branch.branch_deletion:
|
||||
report.status = "PASS"
|
||||
|
||||
+3
-2
@@ -9,12 +9,13 @@
|
||||
"Severity": "high",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "devops",
|
||||
"Description": "**GitHub repository default branch** blocks **force pushes** through branch protection.\n\nEvaluates whether the default branch permits force pushes.",
|
||||
"Description": "**GitHub repository default branch** blocks **force pushes** through branch protection.\n\nEvaluates whether the default branch permits force pushes. This protection can be enforced through classic **branch protection** or repository **rulesets**.",
|
||||
"Risk": "Allowing **force pushes on the default branch** erodes **integrity** and **auditability** by enabling history rewrites and deletion of commits. Attackers or insiders can inject unreviewed code, bypass reviews and status checks, and corrupt PRs, risking supply-chain compromise and reduced **availability**.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#allow-force-pushes"
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#allow-force-pushes",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
|
||||
+5
@@ -26,6 +26,11 @@ class repository_default_branch_disallows_force_push(Check):
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} does allow force pushes on default branch ({repo.default_branch.name})."
|
||||
if (
|
||||
repo.default_branch.allow_force_pushes_source
|
||||
== "ruleset_not_active"
|
||||
):
|
||||
report.status_extended = f"Repository {repo.name} has force pushes disallowed in a ruleset on default branch ({repo.default_branch.name}), but the ruleset is not active."
|
||||
|
||||
if not repo.default_branch.allow_force_pushes:
|
||||
report.status = "PASS"
|
||||
|
||||
+3
-2
@@ -9,12 +9,13 @@
|
||||
"Severity": "high",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "devops",
|
||||
"Description": "**GitHub repository default branch** applies **branch protection rules** to **administrators** via `enforce_admins`, holding admin pushes to the same requirements as other contributors (reviews, status checks, and push restrictions).",
|
||||
"Description": "**GitHub repository default branch** applies **branch protection rules** to **administrators** via `enforce_admins`, holding admin pushes to the same requirements as other contributors (reviews, status checks, and push restrictions). This protection can be enforced through classic **branch protection** or repository **rulesets**.",
|
||||
"Risk": "Without admin enforcement, privileged users can bypass reviews and checks, enabling **unauthorized code changes**. A compromised admin token can inject backdoors, alter dependencies, or disable safeguards, undermining **integrity**, exposing secrets (**confidentiality**), and causing outages (**availability**).",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#do-not-allow-bypassing-the-above-settings"
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#do-not-allow-bypassing-the-above-settings",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
|
||||
+2
@@ -26,6 +26,8 @@ class repository_default_branch_protection_applies_to_admins(Check):
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} does not enforce administrators to be subject to the same branch protection rules as other users."
|
||||
if repo.default_branch.enforce_admins_source == "ruleset_not_active":
|
||||
report.status_extended = f"Repository {repo.name} has a ruleset that would apply to administrators, but the ruleset is not active."
|
||||
|
||||
if repo.default_branch.enforce_admins:
|
||||
report.status = "PASS"
|
||||
|
||||
+3
-2
@@ -9,12 +9,13 @@
|
||||
"Severity": "high",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "devops",
|
||||
"Description": "**GitHub repository default branch** has **branch protection rules** enabled to restrict direct changes and require reviewed, validated merges. The evaluation determines whether the default branch enforces such rules.",
|
||||
"Description": "**GitHub repository default branch** has **branch protection rules** enabled to restrict direct changes and require reviewed, validated merges. The evaluation determines whether the default branch enforces such rules. This protection can be enforced through classic **branch protection** or repository **rulesets**.",
|
||||
"Risk": "Without default-branch protection, changes can bypass reviews and checks, enabling:\n- Unauthorized direct pushes/force pushes\n- Malicious code injection and workflow tampering\n- Accidental deletions or unstable releases\nThis undermines code **integrity** and service **availability**.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches"
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
|
||||
+2
@@ -26,6 +26,8 @@ class repository_default_branch_protection_enabled(Check):
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} does not enforce branch protection on default branch ({repo.default_branch.name})."
|
||||
if repo.default_branch.protected_source == "ruleset_not_active":
|
||||
report.status_extended = f"Repository {repo.name} has a ruleset configured on default branch ({repo.default_branch.name}), but the ruleset is not active."
|
||||
|
||||
if repo.default_branch.protected:
|
||||
report.status = "PASS"
|
||||
|
||||
+3
-2
@@ -9,12 +9,13 @@
|
||||
"Severity": "high",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "devops",
|
||||
"Description": "**GitHub repository default branch** requires **Code Owners** approval for pull requests that modify paths declared in `CODEOWNERS`",
|
||||
"Description": "**GitHub repository default branch** requires **Code Owners** approval for pull requests that modify paths declared in `CODEOWNERS`. This protection can be enforced through classic **branch protection** or repository **rulesets**.",
|
||||
"Risk": "Without required **Code Owners** review, non-owners can merge changes to sensitive code, undermining **integrity**.\nThis increases the chance of **malicious code injection**, hidden backdoors, or fragile changes that enable **data exfiltration** or cause outages, impacting confidentiality and availability.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-review-from-code-owners",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule"
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
|
||||
+5
@@ -30,6 +30,11 @@ class repository_default_branch_requires_codeowners_review(Check):
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} does not require code owner approval for changes to owned code."
|
||||
if (
|
||||
repo.default_branch.require_code_owner_reviews_source
|
||||
== "ruleset_not_active"
|
||||
):
|
||||
report.status_extended = f"Repository {repo.name} has code owner approval configured in a ruleset, but the ruleset is not active."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
+3
-2
@@ -9,12 +9,13 @@
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "devops",
|
||||
"Description": "**GitHub repository default branch** uses **branch protection** to require **conversation resolution** on pull requests (`Require conversation resolution before merging`).",
|
||||
"Description": "**GitHub repository default branch** uses **branch protection** to require **conversation resolution** on pull requests (`Require conversation resolution before merging`). This protection can be enforced through classic **branch protection** or repository **rulesets**.",
|
||||
"Risk": "Unresolved threads let code with known concerns reach default, weakening **integrity** and **confidentiality**. Insecure changes or secrets may ship, enabling injection, auth bypass, or data exposure. **Availability** can suffer from regressions; review accountability is reduced.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches"
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
|
||||
+5
@@ -26,6 +26,11 @@ class repository_default_branch_requires_conversation_resolution(Check):
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} does not require conversation resolution on default branch ({repo.default_branch.name})."
|
||||
if (
|
||||
repo.default_branch.conversation_resolution_source
|
||||
== "ruleset_not_active"
|
||||
):
|
||||
report.status_extended = f"Repository {repo.name} has conversation resolution configured in a ruleset on default branch ({repo.default_branch.name}), but the ruleset is not active."
|
||||
|
||||
if repo.default_branch.conversation_resolution:
|
||||
report.status = "PASS"
|
||||
|
||||
+3
-2
@@ -9,12 +9,13 @@
|
||||
"Severity": "low",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "devops",
|
||||
"Description": "**GitHub repository default branch** enforces `Require linear history`, blocking merge commits and allowing only `squash` or `rebase` merges",
|
||||
"Description": "**GitHub repository default branch** enforces `Require linear history`, blocking merge commits and allowing only `squash` or `rebase` merges. This protection can be enforced through classic **branch protection** or repository **rulesets**.",
|
||||
"Risk": "Without a **linear history**, commit provenance is harder to verify, weakening **integrity** and **accountability**.\n\nMerge commits can obscure diffs entering the default branch, hindering audits and rollbacks, enabling unnoticed **malicious or unreviewed code**, and delaying incident response.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-linear-history",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule"
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
|
||||
+5
@@ -26,6 +26,11 @@ class repository_default_branch_requires_linear_history(Check):
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} does not require linear history on default branch ({repo.default_branch.name})."
|
||||
if (
|
||||
repo.default_branch.required_linear_history_source
|
||||
== "ruleset_not_active"
|
||||
):
|
||||
report.status_extended = f"Repository {repo.name} has linear history configured in a ruleset on default branch ({repo.default_branch.name}), but the ruleset is not active."
|
||||
|
||||
if repo.default_branch.required_linear_history:
|
||||
report.status = "PASS"
|
||||
|
||||
+3
-2
@@ -9,12 +9,13 @@
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "devops",
|
||||
"Description": "**GitHub repository default branch** enforces **required reviews** with a minimum of `2` approving reviews before a pull request can be merged.\n\nAssesses whether an approval threshold of at least `2` is configured for code changes targeting the default branch.",
|
||||
"Description": "**GitHub repository default branch** enforces **required reviews** with a minimum of `2` approving reviews before a pull request can be merged.\n\nAssesses whether an approval threshold of at least `2` is configured for code changes targeting the default branch. This protection can be enforced through classic **branch protection** or repository **rulesets**.",
|
||||
"Risk": "Without multi-review approval on the default branch, a single actor can merge changes, degrading **integrity** and **accountability**. This enables:\n- supply-chain tampering or backdoors\n- introduction of exploitable bugs\n- bypass of change control via compromised accounts",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/approving-a-pull-request-with-required-reviews",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-pull-request-reviews-before-merging"
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-pull-request-reviews-before-merging",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
|
||||
+2
@@ -26,6 +26,8 @@ class repository_default_branch_requires_multiple_approvals(Check):
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} does not enforce at least 2 approvals for code changes."
|
||||
if repo.default_branch.approval_count_source == "ruleset_not_active":
|
||||
report.status_extended = f"Repository {repo.name} has at least 2 approvals configured in a ruleset, but the ruleset is not active."
|
||||
|
||||
if repo.default_branch.approval_count >= 2:
|
||||
report.status = "PASS"
|
||||
|
||||
+3
-2
@@ -9,12 +9,13 @@
|
||||
"Severity": "high",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "devops",
|
||||
"Description": "**GitHub repository default branch** enforces **signed and verified commits** (`Require signed commits`), allowing only commits with valid cryptographic signatures to be pushed or merged.",
|
||||
"Description": "**GitHub repository default branch** enforces **signed and verified commits** (`Require signed commits`), allowing only commits with valid cryptographic signatures to be pushed or merged. This protection can be enforced through classic **branch protection** or repository **rulesets**.",
|
||||
"Risk": "Without required signing, commit authorship can be spoofed and unverified changes added, impacting integrity.\n- Backdoor injection\n- History tampering and forged identities\n- Release pipeline abuse and supply-chain compromise",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-signed-commits",
|
||||
"https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification"
|
||||
"https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
|
||||
+5
@@ -26,6 +26,11 @@ class repository_default_branch_requires_signed_commits(Check):
|
||||
report = CheckReportGithub(metadata=self.metadata(), resource=repo)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} does not require signed commits on default branch ({repo.default_branch.name})."
|
||||
if (
|
||||
repo.default_branch.require_signed_commits_source
|
||||
== "ruleset_not_active"
|
||||
):
|
||||
report.status_extended = f"Repository {repo.name} has signed commits configured in a ruleset on default branch ({repo.default_branch.name}), but the ruleset is not active."
|
||||
|
||||
if repo.default_branch.require_signed_commits:
|
||||
report.status = "PASS"
|
||||
|
||||
+3
-2
@@ -9,13 +9,14 @@
|
||||
"Severity": "high",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "devops",
|
||||
"Description": "**GitHub repository default branch** uses **required status checks**, indicating whether merges are gated by successful check results on pull requests",
|
||||
"Description": "**GitHub repository default branch** uses **required status checks**, indicating whether merges are gated by successful check results on pull requests. This protection can be enforced through classic **branch protection** or repository **rulesets**.",
|
||||
"Risk": "Without required checks, unvetted commits can be merged, degrading code **integrity** and **availability**. Skipped or failing validations may introduce vulnerable dependencies, break builds, or allow malicious code, enabling supply-chain compromise and rapid propagation to production.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-status-checks-before-merging",
|
||||
"https://docs.gearset.com/en/articles/2437757-managing-status-check-rules-in-github"
|
||||
"https://docs.gearset.com/en/articles/2437757-managing-status-check-rules-in-github",
|
||||
"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
|
||||
+2
@@ -28,6 +28,8 @@ class repository_default_branch_status_checks_required(Check):
|
||||
report.status_extended = (
|
||||
f"Repository {repo.name} does not enforce status checks."
|
||||
)
|
||||
if repo.default_branch.status_checks_source == "ruleset_not_active":
|
||||
report.status_extended = f"Repository {repo.name} has status checks configured in a ruleset, but the ruleset is not active."
|
||||
|
||||
if repo.default_branch.status_checks:
|
||||
report.status = "PASS"
|
||||
|
||||
@@ -202,15 +202,40 @@ class Repository(GithubService):
|
||||
|
||||
return None
|
||||
|
||||
def _get_dismiss_stale_reviews_from_rulesets(
|
||||
def _evaluate_default_branch_rulesets(
|
||||
self, repo, default_branch: str
|
||||
) -> tuple[Optional[bool], Optional[str]]:
|
||||
"""Evaluate dismiss-stale-review coverage from repository and parent rulesets."""
|
||||
rulesets = self._get_repository_rulesets(repo)
|
||||
if rulesets is None:
|
||||
return None, None
|
||||
) -> dict[str, tuple[Optional[int], str]]:
|
||||
"""Evaluate default-branch protection coverage provided by rulesets.
|
||||
|
||||
has_inactive_ruleset = False
|
||||
Fetches the repository (and parent) rulesets once and walks every rule that
|
||||
targets the default branch, mapping each ruleset rule to its equivalent classic
|
||||
branch-protection attribute.
|
||||
|
||||
Returns:
|
||||
dict mapping a ``Branch`` attribute name to a ``(value, source)`` tuple where
|
||||
``source`` is ``"ruleset"`` (enforced by an active ruleset), ``"ruleset_not_active"``
|
||||
(configured by a ruleset that is not active) or absent when no ruleset addresses
|
||||
the attribute. ``value`` is only meaningful for ``approval_count`` (the enforced
|
||||
review count); for boolean attributes it is ``None`` and the caller derives the
|
||||
value from ``source`` and the attribute polarity.
|
||||
"""
|
||||
result: dict[str, tuple[Optional[int], str]] = {}
|
||||
rulesets = self._get_repository_rulesets(repo)
|
||||
if not rulesets:
|
||||
return result
|
||||
|
||||
active: set[str] = set()
|
||||
inactive: set[str] = set()
|
||||
active_approval_count: Optional[int] = None
|
||||
inactive_approval_count: Optional[int] = None
|
||||
any_active_ruleset = False
|
||||
any_inactive_ruleset = False
|
||||
# A ruleset with no bypass actors applies to everyone, including administrators
|
||||
# (the rulesets equivalent of "enforce admins"). A ruleset that has bypass actors
|
||||
# would not apply to admins even if activated, so it must not drive the
|
||||
# enforce-admins finding in either the active or the inactive case.
|
||||
admins_enforced_active = False
|
||||
admins_configured_inactive = False
|
||||
|
||||
for ruleset in rulesets:
|
||||
if ruleset.get("target") != "branch":
|
||||
@@ -219,26 +244,119 @@ class Repository(GithubService):
|
||||
if not self._ruleset_targets_default_branch(ruleset, default_branch):
|
||||
continue
|
||||
|
||||
enforcement = ruleset.get("enforcement")
|
||||
is_active = enforcement in {"active", "enabled"}
|
||||
is_inactive = enforcement in {"disabled", "evaluate"}
|
||||
if not (is_active or is_inactive):
|
||||
continue
|
||||
|
||||
has_no_bypass_actors = not (ruleset.get("bypass_actors") or [])
|
||||
if is_active:
|
||||
any_active_ruleset = True
|
||||
if has_no_bypass_actors:
|
||||
admins_enforced_active = True
|
||||
else:
|
||||
any_inactive_ruleset = True
|
||||
if has_no_bypass_actors:
|
||||
admins_configured_inactive = True
|
||||
|
||||
bucket = active if is_active else inactive
|
||||
|
||||
for rule in ruleset.get("rules") or []:
|
||||
if rule.get("type") != "pull_request":
|
||||
continue
|
||||
rule_type = rule.get("type")
|
||||
params = rule.get("parameters") or {}
|
||||
|
||||
dismiss_stale_reviews = (rule.get("parameters") or {}).get(
|
||||
"dismiss_stale_reviews_on_push"
|
||||
)
|
||||
if dismiss_stale_reviews is not True:
|
||||
continue
|
||||
if rule_type == "required_linear_history":
|
||||
bucket.add("required_linear_history")
|
||||
elif rule_type == "required_signatures":
|
||||
bucket.add("require_signed_commits")
|
||||
elif rule_type == "required_status_checks":
|
||||
# Only enforced when at least one status check is configured;
|
||||
# an empty list (or just strict policy) requires nothing.
|
||||
if params.get("required_status_checks"):
|
||||
bucket.add("status_checks")
|
||||
elif rule_type == "non_fast_forward":
|
||||
# Presence of the rule disallows force pushes.
|
||||
bucket.add("allow_force_pushes")
|
||||
elif rule_type == "deletion":
|
||||
# Presence of the rule disallows branch deletion.
|
||||
bucket.add("branch_deletion")
|
||||
elif rule_type == "pull_request":
|
||||
bucket.add("require_pull_request")
|
||||
if params.get("require_code_owner_review") is True:
|
||||
bucket.add("require_code_owner_reviews")
|
||||
if params.get("required_review_thread_resolution") is True:
|
||||
bucket.add("conversation_resolution")
|
||||
if params.get("dismiss_stale_reviews_on_push") is True:
|
||||
bucket.add("dismiss_stale_reviews")
|
||||
count = params.get("required_approving_review_count")
|
||||
if isinstance(count, int):
|
||||
if is_active:
|
||||
active_approval_count = max(
|
||||
active_approval_count or 0, count
|
||||
)
|
||||
else:
|
||||
inactive_approval_count = max(
|
||||
inactive_approval_count or 0, count
|
||||
)
|
||||
|
||||
enforcement = ruleset.get("enforcement")
|
||||
if enforcement in {"active", "enabled"}:
|
||||
return True, "ruleset"
|
||||
if enforcement in {"disabled", "evaluate"}:
|
||||
has_inactive_ruleset = True
|
||||
for concept in (
|
||||
"required_linear_history",
|
||||
"require_signed_commits",
|
||||
"status_checks",
|
||||
"allow_force_pushes",
|
||||
"branch_deletion",
|
||||
"require_pull_request",
|
||||
"require_code_owner_reviews",
|
||||
"conversation_resolution",
|
||||
"dismiss_stale_reviews",
|
||||
):
|
||||
if concept in active:
|
||||
result[concept] = (None, "ruleset")
|
||||
elif concept in inactive:
|
||||
result[concept] = (None, "ruleset_not_active")
|
||||
|
||||
if has_inactive_ruleset:
|
||||
return False, "ruleset_not_active"
|
||||
if active_approval_count is not None:
|
||||
result["approval_count"] = (active_approval_count, "ruleset")
|
||||
elif inactive_approval_count is not None:
|
||||
result["approval_count"] = (inactive_approval_count, "ruleset_not_active")
|
||||
|
||||
return None, None
|
||||
if any_active_ruleset:
|
||||
result["protected"] = (None, "ruleset")
|
||||
elif any_inactive_ruleset:
|
||||
result["protected"] = (None, "ruleset_not_active")
|
||||
|
||||
if admins_enforced_active:
|
||||
result["enforce_admins"] = (None, "ruleset")
|
||||
elif admins_configured_inactive:
|
||||
result["enforce_admins"] = (None, "ruleset_not_active")
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _merge_ruleset_bool(
|
||||
classic_value: Optional[bool], ruleset_source: Optional[str], good: bool = True
|
||||
) -> tuple[Optional[bool], Optional[str]]:
|
||||
"""Merge a boolean branch-protection attribute with its ruleset evaluation.
|
||||
|
||||
Args:
|
||||
classic_value: The value resolved from classic branch protection.
|
||||
ruleset_source: ``"ruleset"``, ``"ruleset_not_active"`` or ``None``.
|
||||
good: The compliant value for the attribute (``True`` for positive attributes,
|
||||
``False`` for inverted ones such as ``allow_force_pushes``).
|
||||
|
||||
Returns:
|
||||
A ``(value, source)`` tuple. Classic protection wins when it already satisfies
|
||||
the control; otherwise an active ruleset enforces the compliant value, and an
|
||||
inactive ruleset surfaces the non-compliant value with a ``"ruleset_not_active"``
|
||||
source so checks can explain the gap.
|
||||
"""
|
||||
classic_pass = classic_value == good
|
||||
if ruleset_source == "ruleset":
|
||||
return good, "ruleset"
|
||||
if ruleset_source == "ruleset_not_active" and not classic_pass:
|
||||
return (not good), "ruleset_not_active"
|
||||
return classic_value, ("classic" if classic_pass else None)
|
||||
|
||||
def _list_repositories(self):
|
||||
"""
|
||||
@@ -398,6 +516,17 @@ class Repository(GithubService):
|
||||
conversation_resolution = False
|
||||
dismiss_stale_reviews = False
|
||||
dismiss_stale_reviews_source = None
|
||||
protected_source = None
|
||||
require_pull_request_source = None
|
||||
approval_count_source = None
|
||||
required_linear_history_source = None
|
||||
allow_force_pushes_source = None
|
||||
branch_deletion_source = None
|
||||
require_code_owner_reviews_source = None
|
||||
require_signed_commits_source = None
|
||||
status_checks_source = None
|
||||
enforce_admins_source = None
|
||||
conversation_resolution_source = None
|
||||
try:
|
||||
branch = repo.get_branch(default_branch)
|
||||
if branch.protected:
|
||||
@@ -458,20 +587,98 @@ class Repository(GithubService):
|
||||
f"{repo.full_name}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
if dismiss_stale_reviews is not None:
|
||||
# Branch protection enforced through rulesets is equivalent to classic branch
|
||||
# protection, so merge any ruleset coverage before reporting findings to avoid
|
||||
# false positives for repositories that have migrated to rulesets.
|
||||
if branch_protection is not None:
|
||||
ruleset_eval = self._evaluate_default_branch_rulesets(
|
||||
repo, default_branch
|
||||
)
|
||||
|
||||
branch_protection, protected_source = self._merge_ruleset_bool(
|
||||
branch_protection, ruleset_eval.get("protected", (None, None))[1]
|
||||
)
|
||||
require_pr, require_pull_request_source = self._merge_ruleset_bool(
|
||||
require_pr,
|
||||
ruleset_eval.get("require_pull_request", (None, None))[1],
|
||||
)
|
||||
(
|
||||
ruleset_dismiss_stale_reviews,
|
||||
ruleset_source,
|
||||
) = self._get_dismiss_stale_reviews_from_rulesets(repo, default_branch)
|
||||
if ruleset_dismiss_stale_reviews:
|
||||
required_linear_history,
|
||||
required_linear_history_source,
|
||||
) = self._merge_ruleset_bool(
|
||||
required_linear_history,
|
||||
ruleset_eval.get("required_linear_history", (None, None))[1],
|
||||
)
|
||||
(
|
||||
require_signed_commits,
|
||||
require_signed_commits_source,
|
||||
) = self._merge_ruleset_bool(
|
||||
require_signed_commits,
|
||||
ruleset_eval.get("require_signed_commits", (None, None))[1],
|
||||
)
|
||||
status_checks, status_checks_source = self._merge_ruleset_bool(
|
||||
status_checks, ruleset_eval.get("status_checks", (None, None))[1]
|
||||
)
|
||||
(
|
||||
require_code_owner_reviews,
|
||||
require_code_owner_reviews_source,
|
||||
) = self._merge_ruleset_bool(
|
||||
require_code_owner_reviews,
|
||||
ruleset_eval.get("require_code_owner_reviews", (None, None))[1],
|
||||
)
|
||||
(
|
||||
conversation_resolution,
|
||||
conversation_resolution_source,
|
||||
) = self._merge_ruleset_bool(
|
||||
conversation_resolution,
|
||||
ruleset_eval.get("conversation_resolution", (None, None))[1],
|
||||
)
|
||||
enforce_admins, enforce_admins_source = self._merge_ruleset_bool(
|
||||
enforce_admins,
|
||||
ruleset_eval.get("enforce_admins", (None, None))[1],
|
||||
)
|
||||
allow_force_pushes, allow_force_pushes_source = (
|
||||
self._merge_ruleset_bool(
|
||||
allow_force_pushes,
|
||||
ruleset_eval.get("allow_force_pushes", (None, None))[1],
|
||||
good=False,
|
||||
)
|
||||
)
|
||||
branch_deletion, branch_deletion_source = self._merge_ruleset_bool(
|
||||
branch_deletion,
|
||||
ruleset_eval.get("branch_deletion", (None, None))[1],
|
||||
good=False,
|
||||
)
|
||||
|
||||
# Dismiss stale reviews keeps its dedicated handling so the classic source
|
||||
# set above (when enabled) is preserved.
|
||||
_, dismiss_source = ruleset_eval.get(
|
||||
"dismiss_stale_reviews", (None, None)
|
||||
)
|
||||
if dismiss_source == "ruleset":
|
||||
dismiss_stale_reviews = True
|
||||
dismiss_stale_reviews_source = "ruleset"
|
||||
elif (
|
||||
ruleset_source == "ruleset_not_active" and not dismiss_stale_reviews
|
||||
dismiss_source == "ruleset_not_active" and not dismiss_stale_reviews
|
||||
):
|
||||
dismiss_stale_reviews = False
|
||||
dismiss_stale_reviews_source = "ruleset_not_active"
|
||||
|
||||
# Approval count takes the strongest requirement between classic and rulesets.
|
||||
approval_value, approval_source = ruleset_eval.get(
|
||||
"approval_count", (None, None)
|
||||
)
|
||||
if approval_source == "ruleset" and approval_value is not None:
|
||||
if approval_value > approval_cnt:
|
||||
approval_cnt = approval_value
|
||||
approval_count_source = "ruleset"
|
||||
elif (
|
||||
approval_source == "ruleset_not_active"
|
||||
and (approval_value or 0) >= 2
|
||||
and approval_cnt < 2
|
||||
):
|
||||
approval_count_source = "ruleset_not_active"
|
||||
|
||||
secret_scanning_enabled = False
|
||||
dependabot_alerts_enabled = False
|
||||
try:
|
||||
@@ -517,17 +724,28 @@ class Repository(GithubService):
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=branch_protection,
|
||||
protected_source=protected_source,
|
||||
default_branch=True,
|
||||
require_pull_request=require_pr,
|
||||
require_pull_request_source=require_pull_request_source,
|
||||
approval_count=approval_cnt,
|
||||
approval_count_source=approval_count_source,
|
||||
required_linear_history=required_linear_history,
|
||||
required_linear_history_source=required_linear_history_source,
|
||||
allow_force_pushes=allow_force_pushes,
|
||||
allow_force_pushes_source=allow_force_pushes_source,
|
||||
branch_deletion=branch_deletion,
|
||||
branch_deletion_source=branch_deletion_source,
|
||||
status_checks=status_checks,
|
||||
status_checks_source=status_checks_source,
|
||||
enforce_admins=enforce_admins,
|
||||
enforce_admins_source=enforce_admins_source,
|
||||
conversation_resolution=conversation_resolution,
|
||||
conversation_resolution_source=conversation_resolution_source,
|
||||
require_code_owner_reviews=require_code_owner_reviews,
|
||||
require_code_owner_reviews_source=require_code_owner_reviews_source,
|
||||
require_signed_commits=require_signed_commits,
|
||||
require_signed_commits_source=require_signed_commits_source,
|
||||
dismiss_stale_reviews=dismiss_stale_reviews,
|
||||
dismiss_stale_reviews_source=dismiss_stale_reviews_source,
|
||||
),
|
||||
@@ -599,17 +817,28 @@ class Branch(BaseModel):
|
||||
|
||||
name: str
|
||||
protected: Optional[bool]
|
||||
protected_source: Optional[str] = None
|
||||
default_branch: bool
|
||||
require_pull_request: Optional[bool]
|
||||
require_pull_request_source: Optional[str] = None
|
||||
approval_count: Optional[int]
|
||||
approval_count_source: Optional[str] = None
|
||||
required_linear_history: Optional[bool]
|
||||
required_linear_history_source: Optional[str] = None
|
||||
allow_force_pushes: Optional[bool]
|
||||
allow_force_pushes_source: Optional[str] = None
|
||||
branch_deletion: Optional[bool]
|
||||
branch_deletion_source: Optional[str] = None
|
||||
status_checks: Optional[bool]
|
||||
status_checks_source: Optional[str] = None
|
||||
enforce_admins: Optional[bool]
|
||||
enforce_admins_source: Optional[str] = None
|
||||
require_code_owner_reviews: Optional[bool]
|
||||
require_code_owner_reviews_source: Optional[str] = None
|
||||
require_signed_commits: Optional[bool]
|
||||
require_signed_commits_source: Optional[str] = None
|
||||
conversation_resolution: Optional[bool]
|
||||
conversation_resolution_source: Optional[str] = None
|
||||
dismiss_stale_reviews: Optional[bool]
|
||||
dismiss_stale_reviews_source: Optional[str] = None
|
||||
|
||||
|
||||
+120
@@ -155,3 +155,123 @@ class Test_repository_default_branch_deletion_disabled_test:
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} does deny default branch deletion."
|
||||
)
|
||||
|
||||
def test_active_ruleset_passes(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=False,
|
||||
branch_deletion_source="ruleset",
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
)
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=default_branch,
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=now,
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_deletion_disabled.repository_default_branch_deletion_disabled.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_deletion_disabled.repository_default_branch_deletion_disabled import (
|
||||
repository_default_branch_deletion_disabled,
|
||||
)
|
||||
|
||||
check = repository_default_branch_deletion_disabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_inactive_ruleset_fails(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
branch_deletion_source="ruleset_not_active",
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
)
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=default_branch,
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=now,
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_deletion_disabled.repository_default_branch_deletion_disabled.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_deletion_disabled.repository_default_branch_deletion_disabled import (
|
||||
repository_default_branch_deletion_disabled,
|
||||
)
|
||||
|
||||
check = repository_default_branch_deletion_disabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} has default branch deletion disabled in a ruleset, but the ruleset is not active."
|
||||
)
|
||||
|
||||
+116
@@ -149,3 +149,119 @@ class Test_repository_default_branch_disallows_force_push_test:
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} does deny force pushes on default branch ({default_branch})."
|
||||
)
|
||||
|
||||
def test_active_ruleset_passes(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=1,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=False,
|
||||
allow_force_pushes_source="ruleset",
|
||||
branch_deletion=False,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
require_code_owner_reviews=True,
|
||||
require_signed_commits=True,
|
||||
conversation_resolution=True,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=True,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_disallows_force_push.repository_default_branch_disallows_force_push.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_disallows_force_push.repository_default_branch_disallows_force_push import (
|
||||
repository_default_branch_disallows_force_push,
|
||||
)
|
||||
|
||||
check = repository_default_branch_disallows_force_push()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_inactive_ruleset_fails(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
allow_force_pushes_source="ruleset_not_active",
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_disallows_force_push.repository_default_branch_disallows_force_push.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_disallows_force_push.repository_default_branch_disallows_force_push import (
|
||||
repository_default_branch_disallows_force_push,
|
||||
)
|
||||
|
||||
check = repository_default_branch_disallows_force_push()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} has force pushes disallowed in a ruleset on default branch ({default_branch}), but the ruleset is not active."
|
||||
)
|
||||
|
||||
+116
@@ -149,3 +149,119 @@ class Test_repository_default_branch_protection_applies_to_admins_test:
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} does enforce administrators to be subject to the same branch protection rules as other users."
|
||||
)
|
||||
|
||||
def test_active_ruleset_passes(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=1,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=False,
|
||||
branch_deletion=False,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
enforce_admins_source="ruleset",
|
||||
require_code_owner_reviews=True,
|
||||
require_signed_commits=True,
|
||||
conversation_resolution=True,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=True,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_protection_applies_to_admins.repository_default_branch_protection_applies_to_admins.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_protection_applies_to_admins.repository_default_branch_protection_applies_to_admins import (
|
||||
repository_default_branch_protection_applies_to_admins,
|
||||
)
|
||||
|
||||
check = repository_default_branch_protection_applies_to_admins()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_inactive_ruleset_fails(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
enforce_admins_source="ruleset_not_active",
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_protection_applies_to_admins.repository_default_branch_protection_applies_to_admins.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_protection_applies_to_admins.repository_default_branch_protection_applies_to_admins import (
|
||||
repository_default_branch_protection_applies_to_admins,
|
||||
)
|
||||
|
||||
check = repository_default_branch_protection_applies_to_admins()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} has a ruleset that would apply to administrators, but the ruleset is not active."
|
||||
)
|
||||
|
||||
+116
@@ -149,3 +149,119 @@ class Test_repository_default_branch_protection_enabled_test:
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} does enforce branch protection on default branch ({default_branch})."
|
||||
)
|
||||
|
||||
def test_active_ruleset_passes(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=True,
|
||||
protected_source="ruleset",
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=1,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=False,
|
||||
branch_deletion=False,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
require_code_owner_reviews=True,
|
||||
require_signed_commits=True,
|
||||
conversation_resolution=True,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=True,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_protection_enabled.repository_default_branch_protection_enabled.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_protection_enabled.repository_default_branch_protection_enabled import (
|
||||
repository_default_branch_protection_enabled,
|
||||
)
|
||||
|
||||
check = repository_default_branch_protection_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_inactive_ruleset_fails(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
protected_source="ruleset_not_active",
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_protection_enabled.repository_default_branch_protection_enabled.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_protection_enabled.repository_default_branch_protection_enabled import (
|
||||
repository_default_branch_protection_enabled,
|
||||
)
|
||||
|
||||
check = repository_default_branch_protection_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} has a ruleset configured on default branch ({default_branch}), but the ruleset is not active."
|
||||
)
|
||||
|
||||
+114
@@ -147,3 +147,117 @@ class Test_repository_default_branch_requires_codeowners_review:
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} requires code owner approval for changes to owned code."
|
||||
)
|
||||
|
||||
def test_active_ruleset_passes(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=1,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=False,
|
||||
branch_deletion=False,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
require_code_owner_reviews=True,
|
||||
require_code_owner_reviews_source="ruleset",
|
||||
require_signed_commits=True,
|
||||
conversation_resolution=True,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=True,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_requires_codeowners_review.repository_default_branch_requires_codeowners_review.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_requires_codeowners_review.repository_default_branch_requires_codeowners_review import (
|
||||
repository_default_branch_requires_codeowners_review,
|
||||
)
|
||||
|
||||
check = repository_default_branch_requires_codeowners_review()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_inactive_ruleset_fails(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_code_owner_reviews_source="ruleset_not_active",
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_requires_codeowners_review.repository_default_branch_requires_codeowners_review.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_requires_codeowners_review.repository_default_branch_requires_codeowners_review import (
|
||||
repository_default_branch_requires_codeowners_review,
|
||||
)
|
||||
|
||||
check = repository_default_branch_requires_codeowners_review()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} has code owner approval configured in a ruleset, but the ruleset is not active."
|
||||
)
|
||||
|
||||
+116
@@ -149,3 +149,119 @@ class Test_repository_default_branch_requires_conversation_resolution_test:
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} does require conversation resolution on default branch ({default_branch})."
|
||||
)
|
||||
|
||||
def test_active_ruleset_passes(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=1,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=False,
|
||||
branch_deletion=False,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
require_code_owner_reviews=True,
|
||||
require_signed_commits=True,
|
||||
conversation_resolution=True,
|
||||
conversation_resolution_source="ruleset",
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=True,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_requires_conversation_resolution.repository_default_branch_requires_conversation_resolution.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_requires_conversation_resolution.repository_default_branch_requires_conversation_resolution import (
|
||||
repository_default_branch_requires_conversation_resolution,
|
||||
)
|
||||
|
||||
check = repository_default_branch_requires_conversation_resolution()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_inactive_ruleset_fails(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
conversation_resolution_source="ruleset_not_active",
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_requires_conversation_resolution.repository_default_branch_requires_conversation_resolution.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_requires_conversation_resolution.repository_default_branch_requires_conversation_resolution import (
|
||||
repository_default_branch_requires_conversation_resolution,
|
||||
)
|
||||
|
||||
check = repository_default_branch_requires_conversation_resolution()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} has conversation resolution configured in a ruleset on default branch ({default_branch}), but the ruleset is not active."
|
||||
)
|
||||
|
||||
+116
@@ -149,3 +149,119 @@ class Test_repository_default_branch_requires_linear_history_test:
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} does require linear history on default branch ({default_branch})."
|
||||
)
|
||||
|
||||
def test_active_ruleset_passes(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=1,
|
||||
required_linear_history=True,
|
||||
required_linear_history_source="ruleset",
|
||||
allow_force_pushes=False,
|
||||
branch_deletion=False,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
require_code_owner_reviews=True,
|
||||
require_signed_commits=True,
|
||||
conversation_resolution=True,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=True,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_requires_linear_history.repository_default_branch_requires_linear_history.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_requires_linear_history.repository_default_branch_requires_linear_history import (
|
||||
repository_default_branch_requires_linear_history,
|
||||
)
|
||||
|
||||
check = repository_default_branch_requires_linear_history()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_inactive_ruleset_fails(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
required_linear_history_source="ruleset_not_active",
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_requires_linear_history.repository_default_branch_requires_linear_history.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_requires_linear_history.repository_default_branch_requires_linear_history import (
|
||||
repository_default_branch_requires_linear_history,
|
||||
)
|
||||
|
||||
check = repository_default_branch_requires_linear_history()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} has linear history configured in a ruleset on default branch ({default_branch}), but the ruleset is not active."
|
||||
)
|
||||
|
||||
+114
@@ -207,3 +207,117 @@ class Test_repository_default_branch_requires_multiple_approvals:
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} does enforce at least 2 approvals for code changes."
|
||||
)
|
||||
|
||||
def test_active_ruleset_passes(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=2,
|
||||
approval_count_source="ruleset",
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals import (
|
||||
repository_default_branch_requires_multiple_approvals,
|
||||
)
|
||||
|
||||
check = repository_default_branch_requires_multiple_approvals()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_inactive_ruleset_fails(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name="main",
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=0,
|
||||
approval_count_source="ruleset_not_active",
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals import (
|
||||
repository_default_branch_requires_multiple_approvals,
|
||||
)
|
||||
|
||||
check = repository_default_branch_requires_multiple_approvals()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} has at least 2 approvals configured in a ruleset, but the ruleset is not active."
|
||||
)
|
||||
|
||||
+116
@@ -149,3 +149,119 @@ class Test_repository_default_branch_requires_signed_commits:
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} does require signed commits on default branch ({default_branch})."
|
||||
)
|
||||
|
||||
def test_active_ruleset_passes(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=True,
|
||||
default_branch=True,
|
||||
require_pull_request=True,
|
||||
approval_count=1,
|
||||
required_linear_history=True,
|
||||
allow_force_pushes=False,
|
||||
branch_deletion=False,
|
||||
status_checks=True,
|
||||
enforce_admins=True,
|
||||
require_code_owner_reviews=True,
|
||||
require_signed_commits=True,
|
||||
require_signed_commits_source="ruleset",
|
||||
conversation_resolution=True,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=True,
|
||||
secret_scanning_enabled=True,
|
||||
dependabot_alerts_enabled=True,
|
||||
delete_branch_on_merge=True,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_requires_signed_commits.repository_default_branch_requires_signed_commits.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_requires_signed_commits.repository_default_branch_requires_signed_commits import (
|
||||
repository_default_branch_requires_signed_commits,
|
||||
)
|
||||
|
||||
check = repository_default_branch_requires_signed_commits()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_inactive_ruleset_fails(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
require_signed_commits_source="ruleset_not_active",
|
||||
conversation_resolution=False,
|
||||
),
|
||||
private=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_requires_signed_commits.repository_default_branch_requires_signed_commits.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_requires_signed_commits.repository_default_branch_requires_signed_commits import (
|
||||
repository_default_branch_requires_signed_commits,
|
||||
)
|
||||
|
||||
check = repository_default_branch_requires_signed_commits()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} has signed commits configured in a ruleset on default branch ({default_branch}), but the ruleset is not active."
|
||||
)
|
||||
|
||||
+118
@@ -151,3 +151,121 @@ class Test_repository_default_branch_status_checks_required_test:
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} does enforce status checks."
|
||||
)
|
||||
|
||||
def test_active_ruleset_passes(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
private=False,
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=True,
|
||||
status_checks_source="ruleset",
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
status_checks=True,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
securitymd=True,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_status_checks_required.repository_default_branch_status_checks_required.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_status_checks_required.repository_default_branch_status_checks_required import (
|
||||
repository_default_branch_status_checks_required,
|
||||
)
|
||||
|
||||
check = repository_default_branch_status_checks_required()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_inactive_ruleset_fails(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
owner="account-name",
|
||||
full_name="account-name/repo1",
|
||||
default_branch=Branch(
|
||||
name=default_branch,
|
||||
protected=False,
|
||||
default_branch=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
required_linear_history=False,
|
||||
allow_force_pushes=True,
|
||||
branch_deletion=True,
|
||||
status_checks=False,
|
||||
status_checks_source="ruleset_not_active",
|
||||
enforce_admins=False,
|
||||
require_code_owner_reviews=False,
|
||||
require_signed_commits=False,
|
||||
conversation_resolution=False,
|
||||
),
|
||||
status_checks=False,
|
||||
archived=False,
|
||||
pushed_at=datetime.now(timezone.utc),
|
||||
private=False,
|
||||
securitymd=False,
|
||||
codeowners_exists=False,
|
||||
secret_scanning_enabled=False,
|
||||
dependabot_alerts_enabled=False,
|
||||
delete_branch_on_merge=False,
|
||||
),
|
||||
}
|
||||
|
||||
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_default_branch_status_checks_required.repository_default_branch_status_checks_required.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_default_branch_status_checks_required.repository_default_branch_status_checks_required import (
|
||||
repository_default_branch_status_checks_required,
|
||||
)
|
||||
|
||||
check = repository_default_branch_status_checks_required()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Repository {repo_name} has status checks configured in a ruleset, but the ruleset is not active."
|
||||
)
|
||||
|
||||
@@ -464,7 +464,7 @@ class Test_Repository_ErrorHandling:
|
||||
assert "Rate limit exceeded" in str(mock_logger.error.call_args)
|
||||
|
||||
|
||||
class Test_Repository_DismissStaleReviewsRulesets:
|
||||
class Test_Repository_BranchProtectionRulesets:
|
||||
def setup_method(self):
|
||||
self.repository_service = Repository.__new__(Repository)
|
||||
self.repository_service.provider = set_mocked_github_provider()
|
||||
@@ -550,6 +550,27 @@ class Test_Repository_DismissStaleReviewsRulesets:
|
||||
],
|
||||
}
|
||||
|
||||
def _build_ruleset(
|
||||
self,
|
||||
*,
|
||||
enforcement,
|
||||
include,
|
||||
rules,
|
||||
bypass_actors=None,
|
||||
ruleset_id=201,
|
||||
):
|
||||
return {
|
||||
"id": ruleset_id,
|
||||
"name": "Branch protection ruleset",
|
||||
"target": "branch",
|
||||
"source_type": "Repository",
|
||||
"source": "owner1/repo1",
|
||||
"enforcement": enforcement,
|
||||
"bypass_actors": bypass_actors or [],
|
||||
"conditions": {"ref_name": {"include": include, "exclude": []}},
|
||||
"rules": rules,
|
||||
}
|
||||
|
||||
def test_process_repository_uses_classic_branch_protection(self):
|
||||
repo = self._build_repo(branch_protected=True, dismiss_stale_reviews=True)
|
||||
repos = {}
|
||||
@@ -606,3 +627,320 @@ class Test_Repository_DismissStaleReviewsRulesets:
|
||||
assert (
|
||||
repos[1].default_branch.dismiss_stale_reviews_source == "ruleset_not_active"
|
||||
)
|
||||
|
||||
def test_ruleset_non_fast_forward_disallows_force_push(self):
|
||||
repo = self._build_repo(
|
||||
branch_protected=False,
|
||||
ruleset_details=[
|
||||
self._build_ruleset(
|
||||
enforcement="active",
|
||||
include=["~DEFAULT_BRANCH"],
|
||||
rules=[{"type": "non_fast_forward"}],
|
||||
)
|
||||
],
|
||||
)
|
||||
repos = {}
|
||||
|
||||
self.repository_service._process_repository(repo, repos)
|
||||
|
||||
assert repos[1].default_branch.allow_force_pushes is False
|
||||
assert repos[1].default_branch.allow_force_pushes_source == "ruleset"
|
||||
|
||||
def test_ruleset_non_fast_forward_inactive_keeps_force_push_allowed(self):
|
||||
repo = self._build_repo(
|
||||
branch_protected=False,
|
||||
ruleset_details=[
|
||||
self._build_ruleset(
|
||||
enforcement="disabled",
|
||||
include=["~DEFAULT_BRANCH"],
|
||||
rules=[{"type": "non_fast_forward"}],
|
||||
)
|
||||
],
|
||||
)
|
||||
repos = {}
|
||||
|
||||
self.repository_service._process_repository(repo, repos)
|
||||
|
||||
assert repos[1].default_branch.allow_force_pushes is True
|
||||
assert repos[1].default_branch.allow_force_pushes_source == "ruleset_not_active"
|
||||
|
||||
def test_classic_protection_takes_precedence_over_inactive_ruleset(self):
|
||||
# Classic protection already disallows force pushes, so an inactive ruleset
|
||||
# must not downgrade the result.
|
||||
repo = self._build_repo(
|
||||
branch_protected=True,
|
||||
ruleset_details=[
|
||||
self._build_ruleset(
|
||||
enforcement="disabled",
|
||||
include=["~DEFAULT_BRANCH"],
|
||||
rules=[{"type": "non_fast_forward"}],
|
||||
)
|
||||
],
|
||||
)
|
||||
repos = {}
|
||||
|
||||
self.repository_service._process_repository(repo, repos)
|
||||
|
||||
assert repos[1].default_branch.allow_force_pushes is False
|
||||
assert repos[1].default_branch.allow_force_pushes_source == "classic"
|
||||
|
||||
def test_ruleset_required_signatures_requires_signed_commits(self):
|
||||
repo = self._build_repo(
|
||||
branch_protected=False,
|
||||
ruleset_details=[
|
||||
self._build_ruleset(
|
||||
enforcement="active",
|
||||
include=["~DEFAULT_BRANCH"],
|
||||
rules=[{"type": "required_signatures"}],
|
||||
)
|
||||
],
|
||||
)
|
||||
repos = {}
|
||||
|
||||
self.repository_service._process_repository(repo, repos)
|
||||
|
||||
assert repos[1].default_branch.require_signed_commits is True
|
||||
assert repos[1].default_branch.require_signed_commits_source == "ruleset"
|
||||
|
||||
def test_ruleset_required_linear_history(self):
|
||||
repo = self._build_repo(
|
||||
branch_protected=False,
|
||||
ruleset_details=[
|
||||
self._build_ruleset(
|
||||
enforcement="active",
|
||||
include=["~DEFAULT_BRANCH"],
|
||||
rules=[{"type": "required_linear_history"}],
|
||||
)
|
||||
],
|
||||
)
|
||||
repos = {}
|
||||
|
||||
self.repository_service._process_repository(repo, repos)
|
||||
|
||||
assert repos[1].default_branch.required_linear_history is True
|
||||
assert repos[1].default_branch.required_linear_history_source == "ruleset"
|
||||
|
||||
def test_ruleset_required_status_checks(self):
|
||||
repo = self._build_repo(
|
||||
branch_protected=False,
|
||||
ruleset_details=[
|
||||
self._build_ruleset(
|
||||
enforcement="active",
|
||||
include=["~DEFAULT_BRANCH"],
|
||||
rules=[
|
||||
{
|
||||
"type": "required_status_checks",
|
||||
"parameters": {
|
||||
"required_status_checks": [{"context": "ci/build"}],
|
||||
"strict_required_status_checks_policy": True,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
repos = {}
|
||||
|
||||
self.repository_service._process_repository(repo, repos)
|
||||
|
||||
assert repos[1].default_branch.status_checks is True
|
||||
assert repos[1].default_branch.status_checks_source == "ruleset"
|
||||
|
||||
def test_ruleset_required_status_checks_without_configured_checks(self):
|
||||
# A required_status_checks rule with an empty list enforces nothing, so it
|
||||
# must not be treated as a passing status-checks requirement.
|
||||
repo = self._build_repo(
|
||||
branch_protected=False,
|
||||
ruleset_details=[
|
||||
self._build_ruleset(
|
||||
enforcement="active",
|
||||
include=["~DEFAULT_BRANCH"],
|
||||
rules=[
|
||||
{
|
||||
"type": "required_status_checks",
|
||||
"parameters": {
|
||||
"required_status_checks": [],
|
||||
"strict_required_status_checks_policy": True,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
repos = {}
|
||||
|
||||
self.repository_service._process_repository(repo, repos)
|
||||
|
||||
assert repos[1].default_branch.status_checks is False
|
||||
assert repos[1].default_branch.status_checks_source is None
|
||||
|
||||
def test_ruleset_deletion_disables_branch_deletion(self):
|
||||
repo = self._build_repo(
|
||||
branch_protected=False,
|
||||
ruleset_details=[
|
||||
self._build_ruleset(
|
||||
enforcement="active",
|
||||
include=["~DEFAULT_BRANCH"],
|
||||
rules=[{"type": "deletion"}],
|
||||
)
|
||||
],
|
||||
)
|
||||
repos = {}
|
||||
|
||||
self.repository_service._process_repository(repo, repos)
|
||||
|
||||
assert repos[1].default_branch.branch_deletion is False
|
||||
assert repos[1].default_branch.branch_deletion_source == "ruleset"
|
||||
|
||||
def test_active_ruleset_marks_default_branch_protected(self):
|
||||
repo = self._build_repo(
|
||||
branch_protected=False,
|
||||
ruleset_details=[
|
||||
self._build_ruleset(
|
||||
enforcement="active",
|
||||
include=["~DEFAULT_BRANCH"],
|
||||
rules=[{"type": "non_fast_forward"}],
|
||||
)
|
||||
],
|
||||
)
|
||||
repos = {}
|
||||
|
||||
self.repository_service._process_repository(repo, repos)
|
||||
|
||||
assert repos[1].default_branch.protected is True
|
||||
assert repos[1].default_branch.protected_source == "ruleset"
|
||||
|
||||
def test_ruleset_pull_request_parameters_map_to_attributes(self):
|
||||
repo = self._build_repo(
|
||||
branch_protected=False,
|
||||
ruleset_details=[
|
||||
self._build_ruleset(
|
||||
enforcement="active",
|
||||
include=["~DEFAULT_BRANCH"],
|
||||
rules=[
|
||||
{
|
||||
"type": "pull_request",
|
||||
"parameters": {
|
||||
"dismiss_stale_reviews_on_push": False,
|
||||
"require_code_owner_review": True,
|
||||
"require_last_push_approval": False,
|
||||
"required_approving_review_count": 2,
|
||||
"required_review_thread_resolution": True,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
repos = {}
|
||||
|
||||
self.repository_service._process_repository(repo, repos)
|
||||
|
||||
branch = repos[1].default_branch
|
||||
assert branch.require_pull_request is True
|
||||
assert branch.require_pull_request_source == "ruleset"
|
||||
assert branch.require_code_owner_reviews is True
|
||||
assert branch.require_code_owner_reviews_source == "ruleset"
|
||||
assert branch.conversation_resolution is True
|
||||
assert branch.conversation_resolution_source == "ruleset"
|
||||
assert branch.approval_count == 2
|
||||
assert branch.approval_count_source == "ruleset"
|
||||
|
||||
def test_inactive_ruleset_approval_count_is_fail_signal(self):
|
||||
repo = self._build_repo(
|
||||
branch_protected=False,
|
||||
ruleset_details=[
|
||||
self._build_ruleset(
|
||||
enforcement="disabled",
|
||||
include=["~DEFAULT_BRANCH"],
|
||||
rules=[
|
||||
{
|
||||
"type": "pull_request",
|
||||
"parameters": {
|
||||
"required_approving_review_count": 2,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
repos = {}
|
||||
|
||||
self.repository_service._process_repository(repo, repos)
|
||||
|
||||
assert repos[1].default_branch.approval_count == 0
|
||||
assert repos[1].default_branch.approval_count_source == "ruleset_not_active"
|
||||
|
||||
def test_active_ruleset_without_bypass_actors_applies_to_admins(self):
|
||||
repo = self._build_repo(
|
||||
branch_protected=False,
|
||||
ruleset_details=[
|
||||
self._build_ruleset(
|
||||
enforcement="active",
|
||||
include=["~DEFAULT_BRANCH"],
|
||||
rules=[{"type": "non_fast_forward"}],
|
||||
bypass_actors=[],
|
||||
)
|
||||
],
|
||||
)
|
||||
repos = {}
|
||||
|
||||
self.repository_service._process_repository(repo, repos)
|
||||
|
||||
assert repos[1].default_branch.enforce_admins is True
|
||||
assert repos[1].default_branch.enforce_admins_source == "ruleset"
|
||||
|
||||
def test_active_ruleset_with_bypass_actors_does_not_apply_to_admins(self):
|
||||
repo = self._build_repo(
|
||||
branch_protected=False,
|
||||
ruleset_details=[
|
||||
self._build_ruleset(
|
||||
enforcement="active",
|
||||
include=["~DEFAULT_BRANCH"],
|
||||
rules=[{"type": "non_fast_forward"}],
|
||||
bypass_actors=[
|
||||
{
|
||||
"actor_id": 1,
|
||||
"actor_type": "RepositoryRole",
|
||||
"bypass_mode": "always",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
repos = {}
|
||||
|
||||
self.repository_service._process_repository(repo, repos)
|
||||
|
||||
# Admins can bypass, so the rulesets do not enforce protection for them.
|
||||
assert repos[1].default_branch.enforce_admins is False
|
||||
assert repos[1].default_branch.enforce_admins_source is None
|
||||
|
||||
def test_inactive_ruleset_with_bypass_actors_is_not_admin_fail_signal(self):
|
||||
# A disabled ruleset that has bypass actors would not apply to admins even if
|
||||
# activated, so it must not raise the enforce-admins ruleset_not_active signal.
|
||||
repo = self._build_repo(
|
||||
branch_protected=False,
|
||||
ruleset_details=[
|
||||
self._build_ruleset(
|
||||
enforcement="disabled",
|
||||
include=["~DEFAULT_BRANCH"],
|
||||
rules=[{"type": "non_fast_forward"}],
|
||||
bypass_actors=[
|
||||
{
|
||||
"actor_id": 1,
|
||||
"actor_type": "RepositoryRole",
|
||||
"bypass_mode": "always",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
repos = {}
|
||||
|
||||
self.repository_service._process_repository(repo, repos)
|
||||
|
||||
assert repos[1].default_branch.enforce_admins is False
|
||||
assert repos[1].default_branch.enforce_admins_source is None
|
||||
# The branch is still reported as protected-but-inactive regardless of bypass.
|
||||
assert repos[1].default_branch.protected_source == "ruleset_not_active"
|
||||
|
||||
Reference in New Issue
Block a user