feat(github): add GitHub provider (#5787)

Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
Hugo Pereira Brito
2025-05-13 15:28:01 +02:00
committed by GitHub
parent d548e869fa
commit 413b948ca0
42 changed files with 1371 additions and 13 deletions

View File

@@ -167,6 +167,21 @@ jobs:
run: |
poetry run pytest -n auto --cov=./prowler/providers/kubernetes --cov-report=xml:kubernetes_coverage.xml tests/providers/kubernetes
# Test GitHub
- name: GitHub - Check if any file has changed
id: github-changed-files
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
with:
files: |
./prowler/providers/github/**
./tests/providers/github/**
.poetry.lock
- name: GitHub - Test
if: steps.github-changed-files.outputs.any_changed == 'true'
run: |
poetry run pytest -n auto --cov=./prowler/providers/github --cov-report=xml:github_coverage.xml tests/providers/github
# Test NHN
- name: NHN - Check if any file has changed
id: nhn-changed-files
@@ -216,4 +231,4 @@ jobs:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
flags: prowler
files: ./aws_coverage.xml,./azure_coverage.xml,./gcp_coverage.xml,./kubernetes_coverage.xml,./nhn_coverage.xml,./m365_coverage.xml,./lib_coverage.xml,./config_coverage.xml
files: ./aws_coverage.xml,./azure_coverage.xml,./gcp_coverage.xml,./kubernetes_coverage.xml,./github_coverage.xml,./nhn_coverage.xml,./m365_coverage.xml,./lib_coverage.xml,./config_coverage.xml

View File

@@ -90,6 +90,7 @@ prowler dashboard
| GCP | 79 | 13 | 7 | 3 |
| Azure | 140 | 18 | 8 | 3 |
| Kubernetes | 83 | 7 | 4 | 7 |
| GitHub | 1 | 1 | 1 | 0 |
| M365 | 44 | 2 | 2 | 0 |
| NHN (Unofficial) | 6 | 2 | 1 | 0 |

53
poetry.lock generated
View File

@@ -884,7 +884,6 @@ description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
markers = "platform_python_implementation != \"PyPy\""
files = [
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
@@ -954,6 +953,7 @@ files = [
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
]
markers = {dev = "platform_python_implementation != \"PyPy\""}
[package.dependencies]
pycparser = "*"
@@ -3751,11 +3751,11 @@ description = "C parser in Python"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
markers = "platform_python_implementation != \"PyPy\""
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
markers = {dev = "platform_python_implementation != \"PyPy\""}
[[package]]
name = "pydantic"
@@ -3836,6 +3836,26 @@ files = [
{file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"},
]
[[package]]
name = "pygithub"
version = "2.5.0"
description = "Use the full Github API v3"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "PyGithub-2.5.0-py3-none-any.whl", hash = "sha256:b0b635999a658ab8e08720bdd3318893ff20e2275f6446fcf35bf3f44f2c0fd2"},
{file = "pygithub-2.5.0.tar.gz", hash = "sha256:e1613ac508a9be710920d26eb18b1905ebd9926aa49398e88151c1b526aad3cf"},
]
[package.dependencies]
Deprecated = "*"
pyjwt = {version = ">=2.4.0", extras = ["crypto"]}
pynacl = ">=1.4.0"
requests = ">=2.14.0"
typing-extensions = ">=4.0.0"
urllib3 = ">=1.26.0"
[[package]]
name = "pygments"
version = "2.19.1"
@@ -3922,6 +3942,33 @@ pyyaml = "*"
[package.extras]
extra = ["pygments (>=2.19.1)"]
[[package]]
name = "pynacl"
version = "1.5.0"
description = "Python binding to the Networking and Cryptography (NaCl) library"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"},
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"},
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"},
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"},
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"},
{file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"},
{file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"},
{file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"},
{file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"},
{file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"},
]
[package.dependencies]
cffi = ">=1.4.1"
[package.extras]
docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"]
[[package]]
name = "pyparsing"
version = "3.2.3"
@@ -5407,4 +5454,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">3.9.1,<3.13"
content-hash = "cc15c8ee6b064b3fda85177c0f1c24a57880c401682fe62daefc2d0f4043150a"
content-hash = "f504af1d00a1da9dd65269509daf32f919c13be6c46f25cd9ef9bfba6b9c9a07"

View File

@@ -8,6 +8,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update the compliance list supported for each provider from docs. [(#7694)](https://github.com/prowler-cloud/prowler/pull/7694)
- Allow setting cluster name in in-cluster mode in Kubernetes. [(#7695)](https://github.com/prowler-cloud/prowler/pull/7695)
- Add Prowler ThreatScore for M365 provider. [(#7692)](https://github.com/prowler-cloud/prowler/pull/7692)
- Add GitHub provider. [(#5787)](https://github.com/prowler-cloud/prowler/pull/5787)
### Fixed
- Update CIS 4.0 for M365 provider. [(#7699)](https://github.com/prowler-cloud/prowler/pull/7699)

View File

@@ -96,6 +96,7 @@ from prowler.providers.azure.models import AzureOutputOptions
from prowler.providers.common.provider import Provider
from prowler.providers.common.quick_inventory import run_provider_quick_inventory
from prowler.providers.gcp.models import GCPOutputOptions
from prowler.providers.github.models import GithubOutputOptions
from prowler.providers.kubernetes.models import KubernetesOutputOptions
from prowler.providers.m365.models import M365OutputOptions
from prowler.providers.nhn.models import NHNOutputOptions
@@ -280,6 +281,10 @@ def prowler():
output_options = KubernetesOutputOptions(
args, bulk_checks_metadata, global_provider.identity
)
elif provider == "github":
output_options = GithubOutputOptions(
args, bulk_checks_metadata, global_provider.identity
)
elif provider == "m365":
output_options = M365OutputOptions(
args, bulk_checks_metadata, global_provider.identity

View File

View File

@@ -0,0 +1,7 @@
{
"Framework": "CIS",
"Version": "1.0",
"Provider": "Github",
"Description": "This CIS Benchmark provides prescriptive guidance for establishing a secure configuration posture for securing the Software Supply Chain.",
"Requirements": []
}

View File

@@ -29,6 +29,7 @@ class Provider(str, Enum):
AZURE = "azure"
KUBERNETES = "kubernetes"
M365 = "m365"
GITHUB = "github"
NHN = "nhn"

View File

@@ -0,0 +1,17 @@
### Account, Check and/or Region can be * to apply for all the cases.
### Account == <GitHub Account Name>
### Resources and tags are lists that can have either Regex or Keywords.
### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together.
### Use an alternation Regex to match one of multiple tags with "ORed" logic.
### For each check you can except Accounts, Regions, Resources and/or Tags.
########################### MUTELIST EXAMPLE ###########################
Mutelist:
Accounts:
"account_1":
Checks:
"repository_public_has_securitymd_file":
Regions:
- "*"
Resources:
- "resource_1"
- "resource_2"

View File

@@ -631,7 +631,10 @@ def execute(
)
elif global_provider.type == "kubernetes":
is_finding_muted_args["cluster"] = global_provider.identity.cluster
elif global_provider.type == "github":
is_finding_muted_args["account_name"] = (
global_provider.identity.account_name
)
for finding in check_findings:
is_finding_muted_args["finding"] = finding
finding.muted = global_provider.mutelist.is_finding_muted(

View File

@@ -542,6 +542,37 @@ class Check_Report_Kubernetes(Check_Report):
self.namespace = "cluster-wide"
@dataclass
class CheckReportGithub(Check_Report):
"""Contains the GitHub Check's finding information."""
resource_name: str
resource_id: str
repository: str
def __init__(
self,
metadata: Dict,
resource: Any,
resource_name: str = None,
resource_id: str = None,
repository: str = None,
) -> None:
"""Initialize the GitHub Check's finding information.
Args:
metadata: The metadata of the check.
resource: Basic information about the resource. Defaults to None.
resource_name: The name of the resource related with the finding.
resource_id: The id of the resource related with the finding.
repository: The repository of the resource related with the finding.
"""
super().__init__(metadata, resource)
self.resource_name = resource_name or getattr(resource, "name", "")
self.resource_id = resource_id or getattr(resource, "id", "")
self.repository = repository or getattr(resource, "repository", "")
@dataclass
class CheckReportM365(Check_Report):
"""Contains the M365 Check's finding information."""

View File

@@ -245,6 +245,14 @@ class Finding(BaseModel):
)
output_data["region"] = f"namespace: {check_output.namespace}"
elif provider.type == "github":
output_data["auth_method"] = provider.auth_method
output_data["resource_name"] = check_output.resource_name
output_data["resource_uid"] = check_output.resource_id
output_data["account_name"] = provider.identity.account_name
output_data["account_uid"] = provider.identity.account_id
output_data["region"] = check_output.repository
elif provider.type == "m365":
output_data["auth_method"] = (
f"{provider.identity.identity_type}: {provider.identity.identity_id}"

View File

@@ -544,11 +544,55 @@ class HTML(Output):
)
return ""
@staticmethod
def get_github_assessment_summary(provider: Provider) -> str:
"""
get_github_assessment_summary gets the HTML assessment summary for the provider
Args:
provider (Provider): the provider object
Returns:
str: the HTML assessment summary
"""
try:
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
GitHub Assessment Summary
</div>
<ul class="list-group
list-group-flush">
<li class="list-group-item">
<b>GitHub account:</b> {provider.identity.account_name}
</li>
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
GitHub Credentials
</div>
<ul class="list-group
list-group-flush">
<li class="list-group-item">
<b>GitHub authentication method:</b> {provider.auth_method}
</li>
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_m365_assessment_summary(provider: Provider) -> str:
"""
get_m365_assessment_summary gets the HTML assessment summary for the provider
Args:
provider (Provider): the provider object
@@ -564,7 +608,9 @@ class HTML(Output):
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>M365 Tenant Domain:</b> {provider.identity.tenant_domain}
<b>M365 Tenant Domain:</b> {
provider.identity.tenant_domain
}
</li>
</ul>
</div>
@@ -581,9 +627,14 @@ class HTML(Output):
<li class="list-group-item">
<b>M365 Identity ID:</b> {provider.identity.identity_id}
</li>
{f'''<li class="list-group-item">
{
f'''<li class="list-group-item">
<b>M365 User:</b> {provider.identity.user}
</li>''' if hasattr(provider.identity, 'user') and provider.identity.user is not None else ""}
</li>'''
if hasattr(provider.identity, "user")
and provider.identity.user is not None
else ""
}
</ul>
</div>
</div>"""
@@ -654,6 +705,7 @@ class HTML(Output):
# It is not pretty but useful
# AWS_provider --> aws
# GCP_provider --> gcp
# GitHub_provider --> github
# Azure_provider --> azure
# Kubernetes_provider --> kubernetes

View File

@@ -16,6 +16,8 @@ def stdout_report(finding, color, verbose, status, fix):
details = finding.location.lower()
if finding.check_metadata.Provider == "kubernetes":
details = finding.namespace.lower()
if finding.check_metadata.Provider == "github":
details = finding.repository
if finding.check_metadata.Provider == "m365":
details = finding.location
if finding.check_metadata.Provider == "nhn":

View File

@@ -71,7 +71,7 @@ class Slack:
- logo (str): The logo URL associated with the provider type.
"""
# TODO: support kubernetes
# TODO: support kubernetes, m365, github
try:
identity = ""
logo = aws_logo
@@ -125,7 +125,7 @@ class Slack:
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"\n:white_check_mark: *{stats['total_pass']} Passed findings* ({round(stats['total_pass'] / stats['findings_count'] * 100 , 2)}%)\n",
"text": f"\n:white_check_mark: *{stats['total_pass']} Passed findings* ({round(stats['total_pass'] / stats['findings_count'] * 100, 2)}%)\n",
},
},
{
@@ -145,7 +145,7 @@ class Slack:
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"\n:x: *{stats['total_fail']} Failed findings* ({round(stats['total_fail'] / stats['findings_count'] * 100 , 2)}%)\n ",
"text": f"\n:x: *{stats['total_fail']} Failed findings* ({round(stats['total_fail'] / stats['findings_count'] * 100, 2)}%)\n ",
},
},
{

View File

@@ -11,6 +11,7 @@ from prowler.config.config import (
orange_color,
)
from prowler.lib.logger import logger
from prowler.providers.github.models import GithubAppIdentityInfo, GithubIdentityInfo
def display_summary_table(
@@ -40,6 +41,13 @@ def display_summary_table(
elif provider.type == "kubernetes":
entity_type = "Context"
audited_entities = provider.identity.context
elif provider.type == "github":
if isinstance(provider.identity, GithubIdentityInfo):
entity_type = "User Name"
audited_entities = provider.identity.account_name
elif isinstance(provider.identity, GithubAppIdentityInfo):
entity_type = "App ID"
audited_entities = provider.identity.app_id
elif provider.type == "m365":
entity_type = "Tenant Domain"
audited_entities = provider.identity.tenant_domain

View File

@@ -234,6 +234,15 @@ class Provider(ABC):
mutelist_path=arguments.mutelist_file,
fixer_config=fixer_config,
)
elif "github" in provider_class_name.lower():
provider_class(
personal_access_token=arguments.personal_access_token,
oauth_app_token=arguments.oauth_app_token,
github_app_key=arguments.github_app_key,
github_app_id=arguments.github_app_id,
mutelist_path=arguments.mutelist_file,
config_path=arguments.config_file,
)
except TypeError as error:
logger.critical(

View File

View File

@@ -0,0 +1,95 @@
from prowler.exceptions.exceptions import ProwlerException
# Exceptions codes from 5000 to 5999 are reserved for Github exceptions
class GithubBaseException(ProwlerException):
"""Base class for Github Errors."""
GITHUB_ERROR_CODES = {
(5000, "GithubEnvironmentVariableError"): {
"message": "Github environment variable error",
"remediation": "Check the Github environment variables and ensure they are properly set.",
},
(5001, "GithubNonExistentTokenError"): {
"message": "A Github token is required to authenticate against Github",
"remediation": "Check the Github token and ensure it is properly set up.",
},
(5002, "GithubInvalidTokenError"): {
"message": "Github token provided is not valid",
"remediation": "Check the Github token and ensure it is valid.",
},
(5003, "GithubSetUpSessionError"): {
"message": "Error setting up session",
"remediation": "Check the session setup and ensure it is properly set up.",
},
(5004, "GithubSetUpIdentityError"): {
"message": "Github identity setup error due to bad credentials",
"remediation": "Check credentials and ensure they are properly set up for Github and the identity provider.",
},
(5005, "GithubInvalidCredentialsError"): {
"message": "Github invalid App Key or App ID for GitHub APP login",
"remediation": "Check user and password and ensure they are properly set up as in your Github account.",
},
}
def __init__(self, code, file=None, original_exception=None, message=None):
provider = "Github"
error_info = self.GITHUB_ERROR_CODES.get((code, self.__class__.__name__))
if message:
error_info["message"] = message
super().__init__(
code=code,
source=provider,
file=file,
original_exception=original_exception,
error_info=error_info,
)
class GithubCredentialsError(GithubBaseException):
"""Base class for Github credentials errors."""
def __init__(self, code, file=None, original_exception=None, message=None):
super().__init__(code, file, original_exception, message)
class GithubEnvironmentVariableError(GithubCredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
5000, file=file, original_exception=original_exception, message=message
)
class GithubNonExistentTokenError(GithubCredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
5001, file=file, original_exception=original_exception, message=message
)
class GithubInvalidTokenError(GithubCredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
5002, file=file, original_exception=original_exception, message=message
)
class GithubSetUpSessionError(GithubCredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
5003, file=file, original_exception=original_exception, message=message
)
class GithubSetUpIdentityError(GithubCredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
5004, file=file, original_exception=original_exception, message=message
)
class GithubInvalidCredentialsError(GithubCredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
5005, file=file, original_exception=original_exception, message=message
)

View File

@@ -0,0 +1,360 @@
import os
from os import environ
from typing import Union
from colorama import Fore, Style
from github import Auth, Github, GithubIntegration
from prowler.config.config import (
default_config_file_path,
get_default_mute_file_path,
load_and_validate_config_file,
)
from prowler.lib.logger import logger
from prowler.lib.mutelist.mutelist import Mutelist
from prowler.lib.utils.utils import print_boxes
from prowler.providers.common.models import Audit_Metadata
from prowler.providers.common.provider import Provider
from prowler.providers.github.exceptions.exceptions import (
GithubEnvironmentVariableError,
GithubInvalidCredentialsError,
GithubInvalidTokenError,
GithubSetUpIdentityError,
GithubSetUpSessionError,
)
from prowler.providers.github.lib.mutelist.mutelist import GithubMutelist
from prowler.providers.github.models import (
GithubAppIdentityInfo,
GithubIdentityInfo,
GithubSession,
)
def format_rsa_key(key: str) -> str:
"""
Format an RSA private key by adding line breaks to the key body.
This function takes an RSA private key in PEM format as input and formats it by inserting line breaks every 64 characters in the key body. This formatting is necessary for the GitHub SDK Parser to correctly process the key.
Args:
key (str): The RSA private key in PEM format as a string. The key should start with "-----BEGIN RSA PRIVATE KEY-----" and end with "-----END RSA PRIVATE KEY-----".
Returns:
str: The formatted RSA private key with line breaks added to the key body. If the input key does not have the correct headers, it is returned unchanged.
Example:
>>> key = "-----BEGIN RSA PRIVATE KEY-----XXXXXXXXXXXXX...-----END RSA PRIVATE KEY-----"
>>> formatted_key = format_rsa_key(key)
>>> print(formatted_key)
-----BEGIN RSA PRIVATE KEY-----
XXXXXXXXXXXXX...
-----END RSA PRIVATE KEY-----
"""
if (
key.startswith("-----BEGIN RSA PRIVATE KEY-----")
and key.endswith("-----END RSA PRIVATE KEY-----")
and "\n" not in key
):
# Extract the key body (excluding the headers)
key_body = key[
len("-----BEGIN RSA PRIVATE KEY-----") : len(key)
- len("-----END RSA PRIVATE KEY-----")
].strip()
# Add line breaks to the body
formatted_key_body = "\n".join(
[key_body[i : i + 64] for i in range(0, len(key_body), 64)]
)
# Reconstruct the key with headers and formatted body
return f"-----BEGIN RSA PRIVATE KEY-----\n{formatted_key_body}\n-----END RSA PRIVATE KEY-----"
return key
class GithubProvider(Provider):
"""
GitHub Provider class
This class is responsible for setting up the GitHub provider, including the session, identity, audit configuration, fixer configuration, and mutelist.
Attributes:
_type (str): The type of the provider.
_auth_method (str): The authentication method used by the provider.
_session (GithubSession): The session object for the provider.
_identity (GithubIdentityInfo): The identity information for the provider.
_audit_config (dict): The audit configuration for the provider.
_fixer_config (dict): The fixer configuration for the provider.
_mutelist (Mutelist): The mutelist for the provider.
audit_metadata (Audit_Metadata): The audit metadata for the provider.
"""
_type: str = "github"
_auth_method: str = None
_session: GithubSession
_identity: GithubIdentityInfo
_audit_config: dict
_mutelist: Mutelist
audit_metadata: Audit_Metadata
def __init__(
self,
# Authentication credentials
personal_access_token: str = "",
oauth_app_token: str = "",
github_app_key: str = "",
github_app_id: int = 0,
# Provider configuration
config_path: str = None,
config_content: dict = None,
fixer_config: dict = {},
mutelist_path: str = None,
mutelist_content: dict = None,
):
"""
GitHub Provider constructor
Args:
personal_access_token (str): GitHub personal access token.
oauth_app_token (str): GitHub OAuth App token.
github_app_key (str): GitHub App key.
github_app_id (int): GitHub App ID.
config_path (str): Path to the audit configuration file.
config_content (dict): Audit configuration content.
fixer_config (dict): Fixer configuration content.
mutelist_path (str): Path to the mutelist file.
mutelist_content (dict): Mutelist content.
"""
logger.info("Instantiating GitHub Provider...")
self._session = self.setup_session(
personal_access_token,
oauth_app_token,
github_app_id,
github_app_key,
)
self._identity = self.setup_identity()
# Audit Config
if config_content:
self._audit_config = config_content
else:
if not config_path:
config_path = default_config_file_path
self._audit_config = load_and_validate_config_file(self._type, config_path)
# Fixer Config
self._fixer_config = fixer_config
# Mutelist
if mutelist_content:
self._mutelist = GithubMutelist(
mutelist_content=mutelist_content,
)
else:
if not mutelist_path:
mutelist_path = get_default_mute_file_path(self.type)
self._mutelist = GithubMutelist(
mutelist_path=mutelist_path,
)
Provider.set_global_provider(self)
@property
def auth_method(self):
"""Returns the authentication method for the GitHub provider."""
return self._auth_method
@property
def pat(self):
"""Returns the personal access token for the GitHub provider."""
return self._pat
@property
def session(self):
"""Returns the session object for the GitHub provider."""
return self._session
@property
def identity(self):
"""Returns the identity information for the GitHub provider."""
return self._identity
@property
def type(self):
"""Returns the type of the GitHub provider."""
return self._type
@property
def audit_config(self):
return self._audit_config
@property
def fixer_config(self):
return self._fixer_config
@property
def mutelist(self) -> GithubMutelist:
"""
mutelist method returns the provider's mutelist.
"""
return self._mutelist
def setup_session(
self,
personal_access_token: str = None,
oauth_app_token: str = None,
github_app_id: int = 0,
github_app_key: str = None,
) -> GithubSession:
"""
Returns the GitHub headers responsible authenticating API calls.
Args:
personal_access_token (str): GitHub personal access token.
oauth_app_token (str): GitHub OAuth App token.
github_app_id (int): GitHub App ID.
github_app_key (str): GitHub App key.
Returns:
GithubSession: Authenticated session token for API requests.
"""
session_token = ""
app_key = ""
app_id = 0
try:
# Ensure that at least one authentication method is selected. Default to environment variable for PAT if none is provided.
if personal_access_token:
session_token = personal_access_token
self._auth_method = "Personal Access Token"
elif oauth_app_token:
session_token = oauth_app_token
self._auth_method = "OAuth App Token"
elif github_app_id and github_app_key:
app_id = github_app_id
with open(github_app_key, "r") as rsa_key:
app_key = rsa_key.read()
self._auth_method = "GitHub App Token"
else:
# PAT
logger.info(
"Looking for GITHUB_PERSONAL_ACCESS_TOKEN environment variable as user has not provided any token...."
)
session_token = environ.get("GITHUB_PERSONAL_ACCESS_TOKEN", "")
if session_token:
self._auth_method = "Environment Variable for Personal Access Token"
if not session_token:
# OAUTH
logger.info(
"Looking for GITHUB_OAUTH_TOKEN environment variable as user has not provided any token...."
)
session_token = environ.get("GITHUB_OAUTH_APP_TOKEN", "")
if session_token:
self._auth_method = "Environment Variable for OAuth App Token"
if not session_token:
# APP
logger.info(
"Looking for GITHUB_APP_ID and GITHUB_APP_KEY environment variables as user has not provided any token...."
)
app_id = environ.get("GITHUB_APP_ID", "")
app_key = format_rsa_key(environ.get(r"GITHUB_APP_KEY", ""))
if app_id and app_key:
self._auth_method = (
"Environment Variables for GitHub App Key and ID"
)
if not self._auth_method:
raise GithubEnvironmentVariableError(
file=os.path.basename(__file__),
message="No authentication method selected and not environment variables were found.",
)
credentials = GithubSession(
token=session_token,
key=app_key,
id=app_id,
)
return credentials
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
raise GithubSetUpSessionError(
original_exception=error,
)
def setup_identity(
self,
) -> Union[GithubIdentityInfo, GithubAppIdentityInfo]:
"""
Returns the GitHub identity information
Returns:
GithubIdentityInfo | GithubAppIdentityInfo: An instance of GithubIdentityInfo or GithubAppIdentityInfo containing the identity information.
"""
credentials = self.session
try:
if credentials.token:
auth = Auth.Token(credentials.token)
g = Github(auth=auth)
try:
identity = GithubIdentityInfo(
account_id=g.get_user().id,
account_name=g.get_user().login,
account_url=g.get_user().url,
)
return identity
except Exception as error:
raise GithubInvalidTokenError(
original_exception=error,
)
elif credentials.id != 0 and credentials.key:
auth = Auth.AppAuth(credentials.id, credentials.key)
gi = GithubIntegration(auth=auth)
try:
identity = GithubAppIdentityInfo(app_id=gi.get_app().id)
return identity
except Exception as error:
raise GithubInvalidCredentialsError(
original_exception=error,
)
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
raise GithubSetUpIdentityError(
original_exception=error,
)
def print_credentials(self):
"""
Prints the GitHub credentials.
Usage:
>>> self.print_credentials()
"""
if isinstance(self.identity, GithubIdentityInfo):
report_lines = [
f"GitHub Account: {Fore.YELLOW}{self.identity.account_name}{Style.RESET_ALL}",
f"GitHub Account ID: {Fore.YELLOW}{self.identity.account_id}{Style.RESET_ALL}",
f"Authentication Method: {Fore.YELLOW}{self.auth_method}{Style.RESET_ALL}",
]
elif isinstance(self.identity, GithubAppIdentityInfo):
report_lines = [
f"GitHub App ID: {Fore.YELLOW}{self.identity.app_id}{Style.RESET_ALL}",
f"Authentication Method: {Fore.YELLOW}{self.auth_method}{Style.RESET_ALL}",
]
report_title = (
f"{Style.BRIGHT}Using the GitHub credentials below:{Style.RESET_ALL}"
)
print_boxes(report_lines, report_title)

View File

@@ -0,0 +1,34 @@
def init_parser(self):
"""Init the Github Provider CLI parser"""
github_parser = self.subparsers.add_parser(
"github", parents=[self.common_providers_parser], help="GitHub Provider"
)
github_auth_subparser = github_parser.add_argument_group("Authentication Modes")
# Authentication Modes
github_auth_subparser.add_argument(
"--personal-access-token",
nargs="?",
help="Personal Access Token to log in against GitHub",
default=None,
)
github_auth_subparser.add_argument(
"--oauth-app-token",
nargs="?",
help="OAuth App Token to log in against GitHub",
default=None,
)
# GitHub App Authentication
github_auth_subparser.add_argument(
"--github-app-id",
nargs="?",
help="GitHub App ID to log in against GitHub",
default=None,
)
github_auth_subparser.add_argument(
"--github-app-key",
nargs="?",
help="GitHub App Key Path to log in against GitHub",
default=None,
)

View File

@@ -0,0 +1,18 @@
from prowler.lib.check.models import CheckReportGithub
from prowler.lib.mutelist.mutelist import Mutelist
from prowler.lib.outputs.utils import unroll_dict, unroll_tags
class GithubMutelist(Mutelist):
def is_finding_muted(
self,
finding: CheckReportGithub,
account_name: str,
) -> bool:
return self.is_muted(
account_name,
finding.check_metadata.CheckID,
"*", # TODO: Study regions in GitHub
finding.resource_name,
unroll_dict(unroll_tags(finding.resource_tags)),
)

View File

@@ -0,0 +1,41 @@
from github import Auth, Github, GithubIntegration
from prowler.lib.logger import logger
from prowler.providers.github.github_provider import GithubProvider
class GithubService:
def __init__(
self,
service: str,
provider: GithubProvider,
):
self.clients = self.__set_clients__(
provider.session,
)
self.audit_config = provider.audit_config
self.fixer_config = provider.fixer_config
def __set_clients__(self, session):
clients = []
try:
if session.token:
auth = Auth.Token(session.token)
clients = [Github(auth=auth)]
elif session.key and session.id:
auth = Auth.AppAuth(
session.id,
session.key,
)
gi = GithubIntegration(auth=auth)
for installation in gi.get_installations():
clients.append(installation.get_github_for_installation())
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return clients

View File

@@ -0,0 +1,42 @@
from pydantic import BaseModel
from prowler.config.config import output_file_timestamp
from prowler.providers.common.models import ProviderOutputOptions
class GithubSession(BaseModel):
token: str
key: str
id: str
class GithubIdentityInfo(BaseModel):
account_id: str
account_name: str
account_url: str
class GithubAppIdentityInfo(BaseModel):
app_id: str
class GithubOutputOptions(ProviderOutputOptions):
def __init__(self, arguments, bulk_checks_metadata, identity):
# First call ProviderOutputOptions init
super().__init__(arguments, bulk_checks_metadata)
# TODO move the below if to ProviderOutputOptions
# Check if custom output filename was input, if not, set the default
if (
not hasattr(arguments, "output_filename")
or arguments.output_filename is None
):
if isinstance(identity, GithubIdentityInfo):
self.output_filename = (
f"prowler-output-{identity.account_name}-{output_file_timestamp}"
)
elif isinstance(identity, GithubAppIdentityInfo):
self.output_filename = (
f"prowler-output-{identity.app_id}-{output_file_timestamp}"
)
else:
self.output_filename = arguments.output_filename

View File

@@ -0,0 +1,4 @@
from prowler.providers.common.provider import Provider
from prowler.providers.github.services.repository.repository_service import Repository
repository_client = Repository(Provider.get_global_provider())

View File

@@ -0,0 +1,30 @@
{
"Provider": "github",
"CheckID": "repository_public_has_securitymd_file",
"CheckTitle": "Check if public repositories have a SECURITY.md file",
"CheckType": [],
"ServiceName": "repository",
"SubServiceName": "",
"ResourceIdTemplate": "github:user-id:repository/repository-name",
"Severity": "low",
"ResourceType": "Other",
"Description": "Ensure that public repositories have a SECURITY.md file",
"Risk": "Not having a SECURITY.md file in a public repository may lead to security vulnerabilities being overlooked by users and contributors.",
"RelatedUrl": "https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Add a SECURITY.md file to the root of the repository. The file should contain information on how to report a security vulnerability, the security policy of the repository, and any other relevant information.",
"Url": "https://github.blog/changelog/2019-05-23-security-policy/"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}

View File

@@ -0,0 +1,42 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGithub
from prowler.providers.github.services.repository.repository_client import (
repository_client,
)
class repository_public_has_securitymd_file(Check):
"""Check if a public repository has a SECURITY.md file
This class verifies whether each public repository has a SECURITY.md file.
"""
def execute(self) -> List[CheckReportGithub]:
"""Execute the Github Repository Public Has SECURITY.md File check
Iterates over all public repositories and checks if they have a SECURITY.md file.
Returns:
List[CheckReportGithub]: A list of reports for each repository
"""
findings = []
for repo in repository_client.repositories.values():
if not repo.private:
report = CheckReportGithub(
metadata=self.metadata(), resource=repo, repository=repo.name
)
report.status = "PASS"
report.status_extended = (
f"Repository {repo.name} does have a SECURITY.md file."
)
if not repo.securitymd:
report.status = "FAIL"
report.status_extended = (
f"Repository {repo.name} does not have a SECURITY.md file."
)
findings.append(report)
return findings

View File

@@ -0,0 +1,45 @@
from typing import Optional
from pydantic import BaseModel
from prowler.lib.logger import logger
from prowler.providers.github.lib.service.service import GithubService
class Repository(GithubService):
def __init__(self, provider):
super().__init__(__class__.__name__, provider)
self.repositories = self._list_repositories()
def _list_repositories(self):
logger.info("Repository - Listing Repositories...")
repos = {}
try:
for client in self.clients:
for repo in client.get_user().get_repos():
try:
securitymd_exists = repo.get_contents("SECURITY.md") is not None
except Exception:
securitymd_exists = False
repos[repo.id] = Repo(
id=repo.id,
name=repo.name,
full_name=repo.full_name,
private=repo.private,
securitymd=securitymd_exists,
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return repos
class Repo(BaseModel):
"""Model for Github Repository"""
id: int
name: str
full_name: str
private: bool
securitymd: Optional[bool] = False

View File

@@ -50,6 +50,7 @@ dependencies = [
"pandas==2.2.3",
"py-ocsf-models==0.3.1",
"pydantic==1.10.21",
"pygithub==2.5.0",
"python-dateutil (>=2.9.0.post0,<3.0.0)",
"pytz==2025.1",
"schema==0.7.7",

View File

@@ -328,7 +328,10 @@ class TestOCSF:
assert isinstance(resource_details[0], ResourceDetails)
assert resource_details[0].labels == ["Name:test", "Environment:dev"]
assert resource_details[0].name == finding_output.resource_name
assert resource_details[0].uid == finding_output.resource_uid
assert resource_details[0].data == {
"details": finding_output.resource_details,
"metadata": {}, # TODO: add metadata to the resource details
}
assert resource_details[0].type == finding_output.metadata.ResourceType
assert resource_details[0].cloud_partition == finding_output.partition
assert resource_details[0].region == finding_output.region

View File

@@ -0,0 +1,37 @@
from mock import MagicMock
from prowler.providers.github.github_provider import GithubProvider
from prowler.providers.github.models import GithubIdentityInfo, GithubSession
# GitHub Identity
ACCOUNT_NAME = "account-name"
ACCOUNT_ID = "account-id"
ACCOUNT_URL = "/user"
# GitHub Credentials
PAT_TOKEN = "github-token"
OAUTH_TOKEN = "oauth-token"
APP_ID = "app-id"
APP_KEY = "app-key"
# Mocked GitHub Provider
def set_mocked_github_provider(
auth_method: str = "personal_access",
credentials: GithubSession = GithubSession(token=PAT_TOKEN, id=APP_ID, key=APP_KEY),
identity: GithubIdentityInfo = GithubIdentityInfo(
account_name=ACCOUNT_NAME,
account_id=ACCOUNT_ID,
account_url=ACCOUNT_URL,
),
audit_config: dict = None,
) -> GithubProvider:
provider = MagicMock()
provider.type = "github"
provider.auth_method = auth_method
provider.session = credentials
provider.identity = identity
provider.audit_config = audit_config
return provider

View File

@@ -0,0 +1,137 @@
from unittest.mock import patch
from prowler.config.config import (
default_fixer_config_file_path,
load_and_validate_config_file,
)
from prowler.providers.github.github_provider import GithubProvider
from prowler.providers.github.models import (
GithubAppIdentityInfo,
GithubIdentityInfo,
GithubSession,
)
from tests.providers.github.github_fixtures import (
ACCOUNT_ID,
ACCOUNT_NAME,
ACCOUNT_URL,
APP_ID,
APP_KEY,
OAUTH_TOKEN,
PAT_TOKEN,
)
class TestGitHubProvider:
def test_github_provider_PAT(self):
personal_access_token = PAT_TOKEN
oauth_app_token = None
github_app_id = None
github_app_key = None
fixer_config = load_and_validate_config_file(
"github", default_fixer_config_file_path
)
with (
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_session",
return_value=GithubSession(token=PAT_TOKEN, id="", key=""),
),
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_identity",
return_value=GithubIdentityInfo(
account_id=ACCOUNT_ID,
account_name=ACCOUNT_NAME,
account_url=ACCOUNT_URL,
),
),
):
provider = GithubProvider(
personal_access_token,
oauth_app_token,
github_app_id,
github_app_key,
)
assert provider._type == "github"
assert provider.session == GithubSession(token=PAT_TOKEN, id="", key="")
assert provider.identity == GithubIdentityInfo(
account_name=ACCOUNT_NAME,
account_id=ACCOUNT_ID,
account_url=ACCOUNT_URL,
)
assert provider._audit_config == {}
assert provider._fixer_config == fixer_config
def test_github_provider_OAuth(self):
personal_access_token = None
oauth_app_token = OAUTH_TOKEN
github_app_id = None
github_app_key = None
fixer_config = load_and_validate_config_file(
"github", default_fixer_config_file_path
)
with (
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_session",
return_value=GithubSession(token=OAUTH_TOKEN, id="", key=""),
),
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_identity",
return_value=GithubIdentityInfo(
account_id=ACCOUNT_ID,
account_name=ACCOUNT_NAME,
account_url=ACCOUNT_URL,
),
),
):
provider = GithubProvider(
personal_access_token,
oauth_app_token,
github_app_id,
github_app_key,
)
assert provider._type == "github"
assert provider.session == GithubSession(token=OAUTH_TOKEN, id="", key="")
assert provider.identity == GithubIdentityInfo(
account_name=ACCOUNT_NAME,
account_id=ACCOUNT_ID,
account_url=ACCOUNT_URL,
)
assert provider._audit_config == {}
assert provider._fixer_config == fixer_config
def test_github_provider_App(self):
personal_access_token = None
oauth_app_token = None
github_app_id = APP_ID
github_app_key = APP_KEY
fixer_config = load_and_validate_config_file(
"github", default_fixer_config_file_path
)
with (
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_session",
return_value=GithubSession(token="", id=APP_ID, key=APP_KEY),
),
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_identity",
return_value=GithubAppIdentityInfo(
app_id=APP_ID,
),
),
):
provider = GithubProvider(
personal_access_token,
oauth_app_token,
github_app_id,
github_app_key,
)
assert provider._type == "github"
assert provider.session == GithubSession(token="", id=APP_ID, key=APP_KEY)
assert provider.identity == GithubAppIdentityInfo(app_id=APP_ID)
assert provider._audit_config == {}
assert provider._fixer_config == fixer_config

View File

@@ -0,0 +1,17 @@
### Account, Check and/or Region can be * to apply for all the cases.
### Account == <GitHub Account Name>
### Resources and tags are lists that can have either Regex or Keywords.
### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together.
### Use an alternation Regex to match one of multiple tags with "ORed" logic.
### For each check you can except Accounts, Regions, Resources and/or Tags.
########################### MUTELIST EXAMPLE ###########################
Mutelist:
Accounts:
"account_1":
Checks:
"repository_public_has_securitymd_file":
Regions:
- "*"
Resources:
- "resource_1"
- "resource_2"

View File

@@ -0,0 +1,100 @@
import yaml
from mock import MagicMock
from prowler.providers.github.lib.mutelist.mutelist import GithubMutelist
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
MUTELIST_FIXTURE_PATH = (
"tests/providers/github/lib/mutelist/fixtures/github_mutelist.yaml"
)
class TestGithubMutelist:
def test_get_mutelist_file_from_local_file(self):
mutelist = GithubMutelist(mutelist_path=MUTELIST_FIXTURE_PATH)
with open(MUTELIST_FIXTURE_PATH) as f:
mutelist_fixture = yaml.safe_load(f)["Mutelist"]
assert mutelist.mutelist == mutelist_fixture
assert mutelist.mutelist_file_path == MUTELIST_FIXTURE_PATH
def test_get_mutelist_file_from_local_file_non_existent(self):
mutelist_path = "tests/lib/mutelist/fixtures/not_present"
mutelist = GithubMutelist(mutelist_path=mutelist_path)
assert mutelist.mutelist == {}
assert mutelist.mutelist_file_path == mutelist_path
def test_validate_mutelist_not_valid_key(self):
mutelist_path = MUTELIST_FIXTURE_PATH
with open(mutelist_path) as f:
mutelist_fixture = yaml.safe_load(f)["Mutelist"]
mutelist_fixture["Accounts1"] = mutelist_fixture["Accounts"]
del mutelist_fixture["Accounts"]
mutelist = GithubMutelist(mutelist_content=mutelist_fixture)
assert not mutelist.validate_mutelist()
assert mutelist.mutelist == {}
assert mutelist.mutelist_file_path is None
def test_is_finding_muted(self):
# Mutelist
mutelist_content = {
"Accounts": {
"account_1": {
"Checks": {
"check_test": {
"Regions": ["*"],
"Resources": ["test_resource"],
}
}
}
}
}
mutelist = GithubMutelist(mutelist_content=mutelist_content)
finding = MagicMock()
finding.check_metadata = MagicMock()
finding.check_metadata.CheckID = "check_test"
finding.status = "FAIL"
finding.resource_name = "test_resource"
finding.account_name = "account_1"
finding.location = "test-location"
finding.resource_tags = []
assert mutelist.is_finding_muted(finding, finding.account_name)
def test_mute_finding(self):
# Mutelist
mutelist_content = {
"Accounts": {
"account_1": {
"Checks": {
"check_test": {
"Regions": ["*"],
"Resources": ["test_resource"],
}
}
}
}
}
mutelist = GithubMutelist(mutelist_content=mutelist_content)
finding_1 = generate_finding_output(
check_id="check_test",
status="FAIL",
account_uid="account_1",
resource_uid="test_resource",
resource_tags=[],
)
muted_finding = mutelist.mute_finding(finding=finding_1)
assert muted_finding.status == "MUTED"
assert muted_finding.muted
assert muted_finding.raw["status"] == "FAIL"

View File

@@ -0,0 +1,104 @@
from unittest import mock
from prowler.providers.github.services.repository.repository_service import Repo
from tests.providers.github.github_fixtures import set_mocked_github_provider
class Test_repository_public_has_securitymd_file_test:
def test_no_repositories(self):
repository_client = mock.MagicMock
repository_client.repositories = {}
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_public_has_securitymd_file.repository_public_has_securitymd_file.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_public_has_securitymd_file.repository_public_has_securitymd_file import (
repository_public_has_securitymd_file,
)
check = repository_public_has_securitymd_file()
result = check.execute()
assert len(result) == 0
def test_one_repository_no_securitymd(self):
repository_client = mock.MagicMock
repo_name = "repo1"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
full_name="account-name/repo1",
private=False,
securitymd=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_public_has_securitymd_file.repository_public_has_securitymd_file.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_public_has_securitymd_file.repository_public_has_securitymd_file import (
repository_public_has_securitymd_file,
)
check = repository_public_has_securitymd_file()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
assert result[0].resource_name == "repo1"
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Repository {repo_name} does not have a SECURITY.md file."
)
def test_one_repository_securitymd(self):
repository_client = mock.MagicMock
repo_name = "repo1"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
full_name="account-name/repo1",
private=False,
securitymd=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_public_has_securitymd_file.repository_public_has_securitymd_file.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_public_has_securitymd_file.repository_public_has_securitymd_file import (
repository_public_has_securitymd_file,
)
check = repository_public_has_securitymd_file()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
assert result[0].resource_name == "repo1"
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Repository {repo_name} does have a SECURITY.md file."
)

View File

@@ -0,0 +1,41 @@
from unittest.mock import patch
from prowler.providers.github.services.repository.repository_service import (
Repo,
Repository,
)
from tests.providers.github.github_fixtures import set_mocked_github_provider
def mock_list_repositories(_):
return {
1: Repo(
id=1,
name="repo1",
full_name="account-name/repo1",
private=False,
securitymd=False,
),
}
@patch(
"prowler.providers.github.services.repository.repository_service.Repository._list_repositories",
new=mock_list_repositories,
)
class Test_Repository_Service:
def test_get_client(self):
repository_service = Repository(set_mocked_github_provider())
assert repository_service.clients[0].__class__.__name__ == "Github"
def test_get_service(self):
repository_service = Repository(set_mocked_github_provider())
assert repository_service.__class__.__name__ == "Repository"
def test_list_repositories(self):
repository_service = Repository(set_mocked_github_provider())
assert len(repository_service.repositories) == 1
assert repository_service.repositories[1].name == "repo1"
assert repository_service.repositories[1].full_name == "account-name/repo1"
assert repository_service.repositories[1].private is False
assert repository_service.repositories[1].securitymd is False