Compare commits
41 Commits
f8bededc9b
...
PRWLR-5516
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
147a772bf2 | ||
|
|
793cf78f3b | ||
|
|
7d10727c34 | ||
|
|
0715c659c8 | ||
|
|
33bd651e19 | ||
|
|
5346867a6a | ||
|
|
f174f8af7c | ||
|
|
5205b85c49 | ||
|
|
446b02e009 | ||
|
|
c7ba87faba | ||
|
|
817ab62fe7 | ||
|
|
359a53bf16 | ||
|
|
0ea3a202c4 | ||
|
|
0b8bf9851a | ||
|
|
27a8bb03d3 | ||
|
|
6b7fa9ac4e | ||
|
|
b50ac49a7e | ||
|
|
93ea60cc86 | ||
|
|
9fe50eb264 | ||
|
|
6f961139a1 | ||
|
|
7001789997 | ||
|
|
aa0d82f500 | ||
|
|
b94a535aba | ||
|
|
9c9526af80 | ||
|
|
1d0243652e | ||
|
|
4e64a26ece | ||
|
|
b2e58c3045 | ||
|
|
45668eb53a | ||
|
|
cac7c42f13 | ||
|
|
5ee960e13d | ||
|
|
c11e6449f9 | ||
|
|
fa8a7f44de | ||
|
|
f028e41652 | ||
|
|
b815eeb7ab | ||
|
|
d0701ad0ce | ||
|
|
d4e5b7c7c7 | ||
|
|
d27c5b7190 | ||
|
|
5ce7aa33ad | ||
|
|
2628a4e506 | ||
|
|
27b628152d | ||
|
|
f8f1e6c076 |
@@ -1,6 +1,6 @@
|
||||
import uuid
|
||||
|
||||
from django.db import transaction, connection
|
||||
from django.db import connection, transaction
|
||||
from rest_framework import permissions
|
||||
from rest_framework.exceptions import NotAuthenticated
|
||||
from rest_framework.filters import SearchFilter
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from types import MappingProxyType
|
||||
|
||||
from api.models import Provider
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.models import CheckMetadata
|
||||
|
||||
from api.models import Provider
|
||||
|
||||
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE = {}
|
||||
PROWLER_CHECKS = {}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from rest_framework import status
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework_json_api.exceptions import exception_handler
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
from rest_framework_simplejwt.exceptions import TokenError, InvalidToken
|
||||
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
|
||||
|
||||
|
||||
class ModelValidationError(ValidationError):
|
||||
|
||||
@@ -4,11 +4,11 @@ from typing import Generator, Optional
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.conf import settings
|
||||
from psqlextra.partitioning import (
|
||||
PostgresPartitioningError,
|
||||
PostgresPartitioningManager,
|
||||
PostgresRangePartition,
|
||||
PostgresRangePartitioningStrategy,
|
||||
PostgresTimePartitionSize,
|
||||
PostgresPartitioningError,
|
||||
)
|
||||
from psqlextra.partitioning.config import PostgresPartitioningConfig
|
||||
from uuid6 import UUID
|
||||
|
||||
@@ -2,8 +2,7 @@ from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import DEFAULT_DB_ALIAS
|
||||
from django.db import models
|
||||
from django.db import DEFAULT_DB_ALIAS, models
|
||||
from django.db.backends.ddl_references import Statement, Table
|
||||
|
||||
from api.db_utils import DB_USER, POSTGRES_TENANT_VAR
|
||||
@@ -131,7 +130,9 @@ class RowLevelSecurityConstraint(models.BaseConstraint):
|
||||
path, _, kwargs = super().deconstruct()
|
||||
return (path, (self.target_field,), kwargs)
|
||||
|
||||
def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS): # noqa: F841
|
||||
def validate(
|
||||
self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS
|
||||
): # noqa: F841
|
||||
if not hasattr(instance, "tenant_id"):
|
||||
raise ValidationError(f"{model.__name__} does not have a tenant_id field.")
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from celery import states
|
||||
from celery.signals import before_task_publish
|
||||
from config.celery import celery_app
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import receiver
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
from django_celery_results.backends.database import DatabaseBackend
|
||||
|
||||
from api.models import Provider
|
||||
from config.celery import celery_app
|
||||
|
||||
|
||||
def create_task_result_on_publish(sender=None, headers=None, **kwargs): # noqa: F841
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from unittest.mock import patch
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
import pytest
|
||||
from conftest import TEST_PASSWORD, get_api_tokens, get_authorization_header
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
@patch("api.v1.views.MainRouter.admin_db", new="default")
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from conftest import TEST_PASSWORD, TEST_USER, get_api_tokens, get_authorization_header
|
||||
from django.urls import reverse
|
||||
|
||||
from conftest import TEST_USER, TEST_PASSWORD, get_api_tokens, get_authorization_header
|
||||
|
||||
|
||||
@patch("api.v1.views.schedule_provider_scan")
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from api.compliance import (
|
||||
generate_compliance_overview_template,
|
||||
generate_scan_compliance,
|
||||
get_prowler_provider_checks,
|
||||
get_prowler_provider_compliance,
|
||||
load_prowler_compliance,
|
||||
load_prowler_checks,
|
||||
generate_scan_compliance,
|
||||
generate_compliance_overview_template,
|
||||
load_prowler_compliance,
|
||||
)
|
||||
from api.models import Provider
|
||||
|
||||
@@ -69,7 +69,7 @@ class TestCompliance:
|
||||
|
||||
load_prowler_compliance()
|
||||
|
||||
from api.compliance import PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE, PROWLER_CHECKS
|
||||
from api.compliance import PROWLER_CHECKS, PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE
|
||||
|
||||
assert PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE == {
|
||||
"template_key": "template_value"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import pytest
|
||||
from config.django.base import DATABASE_ROUTERS as PROD_DATABASE_ROUTERS
|
||||
from django.conf import settings
|
||||
from django.db.migrations.recorder import MigrationRecorder
|
||||
from django.db.utils import ConnectionRouter
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.rls import Tenant
|
||||
from config.django.base import DATABASE_ROUTERS as PROD_DATABASE_ROUTERS
|
||||
|
||||
|
||||
class TestMainDatabaseRouter:
|
||||
|
||||
@@ -2,7 +2,7 @@ from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from unittest.mock import patch
|
||||
|
||||
from api.db_utils import enum_to_choices, one_week_from_now, generate_random_token
|
||||
from api.db_utils import enum_to_choices, generate_random_token, one_week_from_now
|
||||
|
||||
|
||||
class TestEnumToChoices:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from unittest.mock import patch, call
|
||||
from unittest.mock import call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.exceptions import InvitationTokenExpiredException
|
||||
from api.models import Invitation, Provider
|
||||
from api.utils import (
|
||||
get_prowler_provider_kwargs,
|
||||
initialize_prowler_provider,
|
||||
merge_dicts,
|
||||
prowler_provider_connection_test,
|
||||
return_prowler_provider,
|
||||
validate_invitation,
|
||||
)
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
from prowler.providers.azure.azure_provider import AzureProvider
|
||||
from prowler.providers.gcp.gcp_provider import GcpProvider
|
||||
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
|
||||
from rest_framework.exceptions import ValidationError, NotFound
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.exceptions import InvitationTokenExpiredException
|
||||
from api.models import Invitation
|
||||
from api.models import Provider
|
||||
from api.utils import (
|
||||
merge_dicts,
|
||||
return_prowler_provider,
|
||||
initialize_prowler_provider,
|
||||
prowler_provider_connection_test,
|
||||
get_prowler_provider_kwargs,
|
||||
)
|
||||
from api.utils import validate_invitation
|
||||
|
||||
|
||||
class TestMergeDicts:
|
||||
|
||||
@@ -7,12 +7,12 @@ from rest_framework_json_api.serializers import ValidationError
|
||||
from uuid6 import UUID
|
||||
|
||||
from api.uuid_utils import (
|
||||
transform_into_uuid7,
|
||||
datetime_to_uuid7,
|
||||
datetime_from_uuid7,
|
||||
uuid7_start,
|
||||
datetime_to_uuid7,
|
||||
transform_into_uuid7,
|
||||
uuid7_end,
|
||||
uuid7_range,
|
||||
uuid7_start,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.exceptions import InvitationTokenExpiredException
|
||||
from api.models import Invitation, Provider
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
from prowler.providers.azure.azure_provider import AzureProvider
|
||||
from prowler.providers.common.models import Connection
|
||||
from prowler.providers.gcp.gcp_provider import GcpProvider
|
||||
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
|
||||
from rest_framework.exceptions import ValidationError, NotFound
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.exceptions import InvitationTokenExpiredException
|
||||
from api.models import Provider, Invitation
|
||||
|
||||
|
||||
def merge_dicts(default_dict: dict, replacement_dict: dict) -> dict:
|
||||
|
||||
@@ -2,9 +2,8 @@ import json
|
||||
import logging
|
||||
from enum import StrEnum
|
||||
|
||||
from django_guid.log_filters import CorrelationId
|
||||
|
||||
from config.env import env
|
||||
from django_guid.log_filters import CorrelationId
|
||||
|
||||
|
||||
class BackendLogger(StrEnum):
|
||||
@@ -39,9 +38,9 @@ class NDJSONFormatter(logging.Formatter):
|
||||
"funcName": record.funcName,
|
||||
"process": record.process,
|
||||
"thread": record.thread,
|
||||
"transaction_id": record.transaction_id
|
||||
if hasattr(record, "transaction_id")
|
||||
else None,
|
||||
"transaction_id": (
|
||||
record.transaction_id if hasattr(record, "transaction_id") else None
|
||||
),
|
||||
}
|
||||
|
||||
# Add REST API extra fields
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from config.django.base import * # noqa
|
||||
from config.env import env
|
||||
|
||||
|
||||
DEBUG = env.bool("DJANGO_DEBUG", default=True)
|
||||
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["*"])
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from config.django.base import * # noqa
|
||||
from config.env import env
|
||||
|
||||
|
||||
DEBUG = env.bool("DJANGO_DEBUG", default=False)
|
||||
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["localhost", "127.0.0.1"])
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from config.django.base import * # noqa
|
||||
from config.env import env
|
||||
|
||||
|
||||
DEBUG = env.bool("DJANGO_DEBUG", default=False)
|
||||
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["localhost", "127.0.0.1"])
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.django.production")
|
||||
import django # noqa: E402
|
||||
|
||||
django.setup()
|
||||
from config.django.production import LOGGING as DJANGO_LOGGERS, DEBUG # noqa: E402
|
||||
from config.custom_logging import BackendLogger # noqa: E402
|
||||
from config.django.production import DEBUG
|
||||
from config.django.production import LOGGING as DJANGO_LOGGERS # noqa: E402
|
||||
|
||||
BIND_ADDRESS = env("DJANGO_BIND_ADDRESS", default="127.0.0.1")
|
||||
PORT = env("DJANGO_PORT", default=8000)
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from django.db import connections as django_connections, connection as django_connection
|
||||
from django.db import connection as django_connection
|
||||
from django.db import connections as django_connections
|
||||
from django.urls import reverse
|
||||
from django_celery_results.models import TaskResult
|
||||
from prowler.lib.check.models import Severity
|
||||
from prowler.lib.outputs.finding import Status
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from api.models import (
|
||||
ComplianceOverview,
|
||||
Finding,
|
||||
)
|
||||
from api.models import (
|
||||
User,
|
||||
Invitation,
|
||||
Membership,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
ProviderSecret,
|
||||
Resource,
|
||||
ResourceTag,
|
||||
Scan,
|
||||
StateChoices,
|
||||
Task,
|
||||
Membership,
|
||||
ProviderSecret,
|
||||
Invitation,
|
||||
ComplianceOverview,
|
||||
User,
|
||||
)
|
||||
from api.rls import Tenant
|
||||
from api.v1.serializers import TokenSerializer
|
||||
from prowler.lib.check.models import Severity
|
||||
from prowler.lib.outputs.finding import Status
|
||||
|
||||
API_JSON_CONTENT_TYPE = "application/vnd.api+json"
|
||||
NO_TENANT_HTTP_STATUS = status.HTTP_401_UNAUTHORIZED
|
||||
@@ -537,9 +536,10 @@ def get_api_tokens(
|
||||
data=json_body,
|
||||
format="vnd.api+json",
|
||||
)
|
||||
return response.json()["data"]["attributes"]["access"], response.json()["data"][
|
||||
"attributes"
|
||||
]["refresh"]
|
||||
return (
|
||||
response.json()["data"]["attributes"]["access"],
|
||||
response.json()["data"]["attributes"]["refresh"],
|
||||
)
|
||||
|
||||
|
||||
def get_authorization_header(access_token: str) -> dict:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from tasks.jobs.connection import check_provider_connection
|
||||
|
||||
from api.models import Provider
|
||||
from tasks.jobs.connection import check_provider_connection
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
1272
poetry.lock
generated
@@ -77,6 +77,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
|
||||
|
||||
|
||||
@@ -259,6 +260,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
|
||||
)
|
||||
|
||||
# Run the quick inventory for the provider if available
|
||||
if hasattr(args, "quick_inventory") and args.quick_inventory:
|
||||
|
||||
0
prowler/compliance/github/__init__.py
Normal file
7
prowler/compliance/github/cis_1.0_github.json
Normal 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": []
|
||||
}
|
||||
@@ -483,6 +483,20 @@ class Check_Report_Kubernetes(Check_Report):
|
||||
self.namespace = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Check_Report_Github(Check_Report):
|
||||
# TODO change class name to CheckReportGitHub
|
||||
"""Contains the GitHub Check's finding information."""
|
||||
|
||||
resource_name: str
|
||||
resource_id: str
|
||||
|
||||
def __init__(self, metadata):
|
||||
super().__init__(metadata)
|
||||
self.resource_name = ""
|
||||
self.resource_id = ""
|
||||
|
||||
|
||||
# Testing Pending
|
||||
def load_check_metadata(metadata_file: str) -> CheckMetadata:
|
||||
"""
|
||||
|
||||
@@ -232,6 +232,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"] = "global"
|
||||
|
||||
# check_output Unique ID
|
||||
# TODO: move this to a function
|
||||
# TODO: in Azure, GCP and K8s there are fidings without resource_name
|
||||
|
||||
@@ -538,6 +538,51 @@ 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_assessment_summary(provider: Provider) -> str:
|
||||
"""
|
||||
|
||||
@@ -13,6 +13,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 = ""
|
||||
|
||||
if (verbose or fix) and (not status or finding.status in status):
|
||||
if finding.muted:
|
||||
|
||||
@@ -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
|
||||
|
||||
# Check if there are findings and that they are not all MANUAL
|
||||
if findings and not all(finding.status == "MANUAL" for finding in findings):
|
||||
|
||||
@@ -211,6 +211,14 @@ 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,
|
||||
config_path=arguments.config_file,
|
||||
)
|
||||
|
||||
except TypeError as error:
|
||||
logger.critical(
|
||||
|
||||
0
prowler/providers/github/__init__.py
Normal file
0
prowler/providers/github/exceptions/__init__.py
Normal file
95
prowler/providers/github/exceptions/exceptions.py
Normal 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
|
||||
)
|
||||
365
prowler/providers/github/github_provider.py
Normal file
@@ -0,0 +1,365 @@
|
||||
import os
|
||||
|
||||
import environ
|
||||
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):
|
||||
"""
|
||||
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-----MIIBOgIBAAJBAK1...-----END RSA PRIVATE KEY-----"
|
||||
>>> formatted_key = format_rsa_key(key)
|
||||
>>> print(formatted_key)
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIBOgIBAAJBAK1...
|
||||
-----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
|
||||
app_key = github_app_key
|
||||
self._auth_method = "GitHub App Token"
|
||||
|
||||
else:
|
||||
env = environ.Env()
|
||||
# PAT
|
||||
logger.error(
|
||||
"GitHub provider: We will look for GITHUB_PERSONAL_ACCESS_TOKEN enviroment variable as you have not provided any token."
|
||||
)
|
||||
session_token = env.str("GITHUB_PERSONAL_ACCESS_TOKEN", "")
|
||||
if session_token:
|
||||
self._auth_method = "Environment Variable for Personal Access Token"
|
||||
|
||||
if not session_token:
|
||||
# OAUTH
|
||||
logger.error(
|
||||
"GitHub provider: We will look for GITHUB_OAUTH_TOKEN enviroment variable as you have not provided any token."
|
||||
)
|
||||
session_token = env.str("GITHUB_OAUTH_APP_TOKEN", "")
|
||||
if session_token:
|
||||
self._auth_method = "Environment Variable for OAuth App Token"
|
||||
|
||||
if not session_token:
|
||||
# APP
|
||||
logger.error(
|
||||
"GitHub provider: We will look for GITHUB_APP_ID and GITHUB_APP_KEY enviroment variables as you have not provided any."
|
||||
)
|
||||
app_id = env.str("GITHUB_APP_ID", "")
|
||||
app_key = format_rsa_key(env.str(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:
|
||||
logger.critical(
|
||||
"GitHub provider: No authentication method selected and not enviroment variables were found."
|
||||
)
|
||||
raise GithubEnvironmentVariableError(
|
||||
file=os.path.basename(__file__),
|
||||
message="No authentication method selected.",
|
||||
)
|
||||
|
||||
credentials = GithubSession(
|
||||
token=session_token,
|
||||
key=app_key,
|
||||
id=app_id,
|
||||
)
|
||||
|
||||
return credentials
|
||||
|
||||
except Exception as error:
|
||||
logger.critical("GitHub provider: Error setting up session.")
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
raise GithubSetUpSessionError(
|
||||
original_exception=error,
|
||||
)
|
||||
|
||||
def setup_identity(
|
||||
self,
|
||||
) -> 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:
|
||||
logger.critical("GitHub provider: Given token is not valid.")
|
||||
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:
|
||||
logger.critical("GitHub provider: Given credentials are not valid.")
|
||||
raise GithubInvalidCredentialsError(
|
||||
original_exception=error,
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.critical("GitHub provider: Error setting up identity.")
|
||||
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)
|
||||
0
prowler/providers/github/lib/arguments/__init__.py
Normal file
69
prowler/providers/github/lib/arguments/arguments.py
Normal file
@@ -0,0 +1,69 @@
|
||||
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"
|
||||
)
|
||||
|
||||
# Authentication Modes
|
||||
github_parser.add_argument(
|
||||
"--personal-access-token",
|
||||
nargs="?",
|
||||
help="Personal Access Token to log in against GitHub",
|
||||
default=None,
|
||||
)
|
||||
|
||||
github_parser.add_argument(
|
||||
"--oauth-app-token",
|
||||
nargs="?",
|
||||
help="OAuth App Token to log in against GitHub",
|
||||
default=None,
|
||||
)
|
||||
|
||||
# GitHub App Authentication
|
||||
github_parser.add_argument(
|
||||
"--github-app-id",
|
||||
nargs="?",
|
||||
help="GitHub App ID to log in against GitHub",
|
||||
default=None,
|
||||
)
|
||||
github_parser.add_argument(
|
||||
"--github-app-key",
|
||||
nargs="?",
|
||||
help="GitHub App Key to log in against GitHub",
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Validation function
|
||||
github_parser.set_defaults(func=validate_github_auth)
|
||||
|
||||
|
||||
def validate_github_auth(self, args):
|
||||
"""Validation for GitHub Authentication."""
|
||||
# Contar los métodos de autenticación utilizados
|
||||
auth_methods = sum(
|
||||
[
|
||||
args.personal_access_token is not None,
|
||||
args.oauth_app_token is not None,
|
||||
args.github_app_id is not None or args.github_app_key is not None,
|
||||
]
|
||||
)
|
||||
|
||||
if auth_methods == 0:
|
||||
raise ValueError(
|
||||
"You must specify at least one authentication method: "
|
||||
"--personal-access-token, --oauth-app-token, or both --github-app-id and --github-app-key."
|
||||
)
|
||||
|
||||
if auth_methods > 1:
|
||||
raise ValueError(
|
||||
"You can only use one authentication method at a time: "
|
||||
"--personal-access-token, --oauth-app-token, or both --github-app-id and --github-app-key."
|
||||
)
|
||||
|
||||
# Validar que ambos parámetros de GitHub App estén presentes si se elige este método
|
||||
if (args.github_app_id is not None or args.github_app_key is not None) and (
|
||||
args.github_app_id is None or args.github_app_key is None
|
||||
):
|
||||
raise ValueError(
|
||||
"Both --github-app-id and --github-app-key must be provided for GitHub App Authentication."
|
||||
)
|
||||
0
prowler/providers/github/lib/mutelist/__init__.py
Normal file
17
prowler/providers/github/lib/mutelist/mutelist.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from prowler.lib.check.models import Check_Report_Github
|
||||
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: Check_Report_Github,
|
||||
) -> bool:
|
||||
return self.is_muted(
|
||||
finding.account_name,
|
||||
finding.check_metadata.CheckID,
|
||||
finding.location,
|
||||
finding.resource_name,
|
||||
unroll_dict(unroll_tags(finding.resource_tags)),
|
||||
)
|
||||
0
prowler/providers/github/lib/service/__init__.py
Normal file
41
prowler/providers/github/lib/service/service.py
Normal 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
|
||||
42
prowler/providers/github/models.py
Normal 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
|
||||
@@ -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())
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Provider": "github",
|
||||
"CheckID": "repository_code_changes_multi_approval_requirement",
|
||||
"CheckTitle": "Check if repositories require at least 2 code changes approvals",
|
||||
"CheckType": [],
|
||||
"ServiceName": "repository",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "github:user-id:repository/repository-name",
|
||||
"Severity": "high",
|
||||
"ResourceType": "Other",
|
||||
"Description": "Ensure that repositories require at least 2 code changes approvals before merging a pull request.",
|
||||
"Risk": "If repositories do not require at least 2 code changes approvals before merging a pull request, it is possible that code changes are not being reviewed by multiple people, which could lead to the introduction of bugs or security vulnerabilities.",
|
||||
"RelatedUrl": "https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/approving-a-pull-request-with-required-reviews",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "To require at least 2 code changes approvals before merging a pull request, navigate to the repository settings, click on 'Branches', and then 'Add rule'.",
|
||||
"Url": "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"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_Github
|
||||
from prowler.providers.github.services.repository.repository_client import (
|
||||
repository_client,
|
||||
)
|
||||
|
||||
|
||||
class repository_code_changes_multi_approval_requirement(Check):
|
||||
"""Check if a repository enforces at least 2 approvals for code changes
|
||||
|
||||
This class verifies whether each repository enforces at least 2 approvals for code changes.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[Check_Report_Github]:
|
||||
"""Execute the Github Repository code changes enforce multi approval requirement check
|
||||
|
||||
Iterates over each repository and checks if the repository enforces at least 2 approvals for code changes.
|
||||
|
||||
Returns:
|
||||
List[Check_Report_Github]: A list of reports for each repository
|
||||
"""
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
report = Check_Report_Github(self.metadata())
|
||||
report.resource_id = repo.id
|
||||
report.resource_name = repo.name
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} does not enforce at least 2 approvals for code changes."
|
||||
|
||||
if repo.default_branch_protection.approval_count >= 2:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Repository {repo.name} does enforce at least 2 approvals for code changes."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Provider": "github",
|
||||
"CheckID": "repository_enforces_default_branch_protection",
|
||||
"CheckTitle": "Check if branch protection is enforced on the default branch ",
|
||||
"CheckType": [],
|
||||
"ServiceName": "repository",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "github:user-id:repository/repository-name",
|
||||
"Severity": "critical",
|
||||
"ResourceType": "Other",
|
||||
"Description": "Ensure branch protection is enforced on the default branch",
|
||||
"Risk": "The absence of branch protection on the default branch increases the risk of unauthorized, unreviewed, or untested changes being merged. This can compromise the stability, security, and reliability of the codebase, which is especially critical for production deployments.",
|
||||
"RelatedUrl": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Apply branch protection rules to the default branch to ensure it is safeguarded against unauthorized or improper modifications. This helps maintain code quality, enforces proper review and testing procedures, and reduces the risk of accidental or malicious changes.",
|
||||
"Url": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule#creating-a-branch-protection-rule"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_Github
|
||||
from prowler.providers.github.services.repository.repository_client import (
|
||||
repository_client,
|
||||
)
|
||||
|
||||
|
||||
class repository_enforces_default_branch_protection(Check):
|
||||
"""Check if a repository enforces default branch protection
|
||||
|
||||
This class verifies whether each repository enforces default branch protection.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[Check_Report_Github]:
|
||||
"""Execute the Github Repository Enforces Default Branch Protection check
|
||||
|
||||
Iterates over all repositories and checks if they enforce default branch protection.
|
||||
|
||||
Returns:
|
||||
List[Check_Report_Github]: A list of reports for each repository
|
||||
"""
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
report = Check_Report_Github(self.metadata())
|
||||
report.resource_id = repo.id
|
||||
report.resource_name = repo.name
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Repository {repo.name} does not enforce branch protection on default branch ({repo.default_branch})."
|
||||
|
||||
if repo.default_branch_protection:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Repository {repo.name} does enforce branch protection on default branch ({repo.default_branch})."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -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/quickstart-for-securing-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://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_Github
|
||||
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[Check_Report_Github]:
|
||||
"""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[Check_Report_Github]: A list of reports for each repository
|
||||
"""
|
||||
findings = []
|
||||
for repo in repository_client.repositories.values():
|
||||
if not repo.private:
|
||||
report = Check_Report_Github(self.metadata())
|
||||
report.resource_id = repo.id
|
||||
report.resource_name = 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
|
||||
@@ -0,0 +1,89 @@
|
||||
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():
|
||||
if not repo.private: # Only for testing purposes
|
||||
default_branch = repo.default_branch
|
||||
securitymd_exists = False
|
||||
try:
|
||||
securitymd_exists = (
|
||||
repo.get_contents("SECURITY.md") is not None
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Could not find SECURITY.md for repo {repo.name}: {e}"
|
||||
)
|
||||
|
||||
branch_protection = None
|
||||
try:
|
||||
branch = repo.get_branch(default_branch)
|
||||
if branch.protected:
|
||||
protection = branch.get_protection()
|
||||
if protection:
|
||||
require_pr = (
|
||||
protection.required_pull_request_reviews
|
||||
is not None
|
||||
)
|
||||
approval_cnt = (
|
||||
protection.required_pull_request_reviews.required_approving_review_count
|
||||
if require_pr
|
||||
else 0
|
||||
)
|
||||
branch_protection = Protection(
|
||||
require_pull_request=require_pr,
|
||||
approval_count=approval_cnt,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Could not get branch protection for repo {repo.name}: {e}"
|
||||
)
|
||||
|
||||
repos[repo.id] = Repo(
|
||||
id=repo.id,
|
||||
name=repo.name,
|
||||
full_name=repo.full_name,
|
||||
default_branch=repo.default_branch,
|
||||
private=repo.private,
|
||||
securitymd=securitymd_exists,
|
||||
default_branch_protection=branch_protection,
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return repos
|
||||
|
||||
|
||||
class Protection(BaseModel):
|
||||
"""Model for Github Branch Protection"""
|
||||
|
||||
require_pull_request: Optional[bool] = False
|
||||
approval_count: Optional[int] = 0
|
||||
|
||||
|
||||
class Repo(BaseModel):
|
||||
"""Model for Github Repository"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
full_name: str
|
||||
private: bool
|
||||
default_branch: str
|
||||
default_branch_protection: Optional[Protection]
|
||||
securitymd: Optional[bool] = False
|
||||
@@ -65,6 +65,7 @@ numpy = "2.0.2"
|
||||
pandas = "2.2.3"
|
||||
py-ocsf-models = "0.2.0"
|
||||
pydantic = "1.10.18"
|
||||
pygithub = "2.5.0"
|
||||
python = ">=3.9,<3.13"
|
||||
python-dateutil = "^2.9.0.post0"
|
||||
pytz = "2024.2"
|
||||
|
||||
@@ -739,7 +739,6 @@ class TestGCPProvider:
|
||||
), patch(
|
||||
"prowler.providers.gcp.gcp_provider.GcpProvider.validate_project_id"
|
||||
) as mock_validate_project_id:
|
||||
|
||||
mock_validate_project_id.side_effect = GCPInvalidProviderIdError(
|
||||
"Invalid project ID"
|
||||
)
|
||||
|
||||
37
tests/providers/github/github_fixtures.py
Normal 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
|
||||
137
tests/providers/github/github_provider_test.py
Normal 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
|
||||
@@ -0,0 +1,157 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import (
|
||||
Protection,
|
||||
Repo,
|
||||
)
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
class Test_repository_code_changes_multi_approval_requirement:
|
||||
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_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement import (
|
||||
repository_code_changes_multi_approval_requirement,
|
||||
)
|
||||
|
||||
check = repository_code_changes_multi_approval_requirement()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_repository_no_require_pull_request(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,
|
||||
default_branch="main",
|
||||
default_branch_protection=Protection(
|
||||
require_pull_request=False, approval_count=0
|
||||
),
|
||||
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_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement import (
|
||||
repository_code_changes_multi_approval_requirement,
|
||||
)
|
||||
|
||||
check = repository_code_changes_multi_approval_requirement()
|
||||
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 enforce at least 2 approvals for code changes."
|
||||
)
|
||||
|
||||
def test_repository_no_approvals(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,
|
||||
default_branch="master",
|
||||
default_branch_protection=Protection(
|
||||
require_pull_request=True, approval_count=0
|
||||
),
|
||||
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_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement import (
|
||||
repository_code_changes_multi_approval_requirement,
|
||||
)
|
||||
|
||||
check = repository_code_changes_multi_approval_requirement()
|
||||
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 enforce at least 2 approvals for code changes."
|
||||
)
|
||||
|
||||
def test_repository_two_approvals(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,
|
||||
default_branch="master",
|
||||
default_branch_protection=Protection(
|
||||
require_pull_request=True, approval_count=2
|
||||
),
|
||||
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_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement import (
|
||||
repository_code_changes_multi_approval_requirement,
|
||||
)
|
||||
|
||||
check = repository_code_changes_multi_approval_requirement()
|
||||
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 enforce at least 2 approvals for code changes."
|
||||
)
|
||||
@@ -0,0 +1,114 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import (
|
||||
Protection,
|
||||
Repo,
|
||||
)
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
class Test_repository_enforces_default_branch_protection_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_enforces_default_branch_protection.repository_enforces_default_branch_protection.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_enforces_default_branch_protection.repository_enforces_default_branch_protection import (
|
||||
repository_enforces_default_branch_protection,
|
||||
)
|
||||
|
||||
check = repository_enforces_default_branch_protection()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_without_default_branch_protection(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
full_name="account-name/repo1",
|
||||
default_branch=default_branch,
|
||||
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_enforces_default_branch_protection.repository_enforces_default_branch_protection.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_enforces_default_branch_protection.repository_enforces_default_branch_protection import (
|
||||
repository_enforces_default_branch_protection,
|
||||
)
|
||||
|
||||
check = repository_enforces_default_branch_protection()
|
||||
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 enforce branch protection on default branch ({default_branch})."
|
||||
)
|
||||
|
||||
def test_default_branch_protection(self):
|
||||
repository_client = mock.MagicMock
|
||||
repo_name = "repo1"
|
||||
default_branch = "main"
|
||||
repository_client.repositories = {
|
||||
1: Repo(
|
||||
id=1,
|
||||
name=repo_name,
|
||||
full_name="account-name/repo1",
|
||||
private=False,
|
||||
default_branch=default_branch,
|
||||
default_branch_protection=Protection(
|
||||
require_pull_request=True, approval_count=2
|
||||
),
|
||||
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_enforces_default_branch_protection.repository_enforces_default_branch_protection.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_enforces_default_branch_protection.repository_enforces_default_branch_protection import (
|
||||
repository_enforces_default_branch_protection,
|
||||
)
|
||||
|
||||
check = repository_enforces_default_branch_protection()
|
||||
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 enforce branch protection on default branch ({default_branch})."
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
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",
|
||||
default_branch="main",
|
||||
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",
|
||||
default_branch="main",
|
||||
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."
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import (
|
||||
Protection,
|
||||
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,
|
||||
default_branch="main",
|
||||
default_branch_protection=Protection(
|
||||
require_pull_request=True, approval_count=2
|
||||
),
|
||||
securitymd=True,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@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].default_branch == "main"
|
||||
# Default branch protection
|
||||
assert repository_service.repositories[
|
||||
1
|
||||
].default_branch_protection.require_pull_request
|
||||
assert (
|
||||
repository_service.repositories[1].default_branch_protection.approval_count
|
||||
== 2
|
||||
)
|
||||
# Repo
|
||||
assert repository_service.repositories[1].securitymd
|
||||
@@ -17,4 +17,4 @@ build
|
||||
!jest.config.js
|
||||
!plopfile.js
|
||||
!react-shim.js
|
||||
!tsup.config.ts
|
||||
!tsup.config.ts
|
||||
|
||||
@@ -1 +1 @@
|
||||
node_modules/
|
||||
node_modules/
|
||||
|
||||
@@ -6,5 +6,7 @@
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"printWidth": 80,
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
"plugins": [
|
||||
"prettier-plugin-tailwindcss"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M12.506 8.407C5.603 8.407 0 14.01 0 20.912l.097 34.74h11.117l-.097-23.1v-11.63a1.4 1.4 0 0 1 1.25-1.39c.047-.005.09 0 .14 0 .383 0 .73.157.98.408L42.653 51.93c2.342 2.35 5.523 3.668 8.84 3.665C58.397 55.594 64 49.99 64 43.088V8.35H52.883V43.14a1.39 1.39 0 0 1-1.39 1.337c-.368 0-.72-.147-.98-.408L21.347 12.07c-2.342-2.35-5.524-3.668-8.84-3.664z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M12.506 8.407C5.603 8.407 0 14.01 0 20.912l.097 34.74h11.117l-.097-23.1v-11.63a1.4 1.4 0 0 1 1.25-1.39c.047-.005.09 0 .14 0 .383 0 .73.157.98.408L42.653 51.93c2.342 2.35 5.523 3.668 8.84 3.665C58.397 55.594 64 49.99 64 43.088V8.35H52.883V43.14a1.39 1.39 0 0 1-1.39 1.337c-.368 0-.72-.147-.98-.408L21.347 12.07c-2.342-2.35-5.524-3.668-8.84-3.664z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 426 B After Width: | Height: | Size: 427 B |
|
Before Width: | Height: | Size: 547 KiB After Width: | Height: | Size: 547 KiB |
@@ -146,7 +146,7 @@ export const LaunchScanWorkflow = ({
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/*
|
||||
{/*
|
||||
<div className="flex flex-col justify-start">
|
||||
<AnimatePresence>
|
||||
{form.watch("providerId") && (
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "001",
|
||||
"alias": "Amazon EC2",
|
||||
"regions": ["us-east-1", "us-east-2"],
|
||||
"regions": [
|
||||
"us-east-1",
|
||||
"us-east-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 3,
|
||||
"last_checked_at": null
|
||||
@@ -32,7 +35,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "002",
|
||||
"alias": "Amazon EMR",
|
||||
"regions": ["us-west-1", "us-west-2"],
|
||||
"regions": [
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 0,
|
||||
"last_checked_at": null
|
||||
@@ -49,7 +55,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "003",
|
||||
"alias": "Amazon GuardDuty",
|
||||
"regions": ["eu-west-1", "eu-west-2"],
|
||||
"regions": [
|
||||
"eu-west-1",
|
||||
"eu-west-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 5,
|
||||
"last_checked_at": null
|
||||
@@ -66,7 +75,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "004",
|
||||
"alias": "Amazon Inspector",
|
||||
"regions": ["ap-southeast-1", "ap-southeast-2"],
|
||||
"regions": [
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 0,
|
||||
"last_checked_at": null
|
||||
@@ -83,7 +95,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "005",
|
||||
"alias": "Amazon Macie",
|
||||
"regions": ["eu-north-1", "eu-north-2"],
|
||||
"regions": [
|
||||
"eu-north-1",
|
||||
"eu-north-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 2,
|
||||
"last_checked_at": null
|
||||
@@ -100,7 +115,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "006",
|
||||
"alias": "Amazon RDS",
|
||||
"regions": ["sa-east-1", "sa-east-2"],
|
||||
"regions": [
|
||||
"sa-east-1",
|
||||
"sa-east-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 0,
|
||||
"last_checked_at": null
|
||||
@@ -117,7 +135,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "007",
|
||||
"alias": "Amazon Route 53",
|
||||
"regions": ["af-south-1", "af-south-2"],
|
||||
"regions": [
|
||||
"af-south-1",
|
||||
"af-south-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 1,
|
||||
"last_checked_at": null
|
||||
@@ -134,7 +155,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "008",
|
||||
"alias": "Amazon S3",
|
||||
"regions": ["us-east-1", "us-west-1"],
|
||||
"regions": [
|
||||
"us-east-1",
|
||||
"us-west-1"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 3,
|
||||
"last_checked_at": null
|
||||
@@ -151,7 +175,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "009",
|
||||
"alias": "Amazon SNS",
|
||||
"regions": ["eu-central-1", "eu-central-2"],
|
||||
"regions": [
|
||||
"eu-central-1",
|
||||
"eu-central-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 0,
|
||||
"last_checked_at": null
|
||||
@@ -168,7 +195,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "010",
|
||||
"alias": "Amazon VPC",
|
||||
"regions": ["us-west-1", "us-west-2"],
|
||||
"regions": [
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 4,
|
||||
"last_checked_at": null
|
||||
@@ -185,7 +215,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "011",
|
||||
"alias": "AWS Account",
|
||||
"regions": ["eu-west-1", "eu-west-3"],
|
||||
"regions": [
|
||||
"eu-west-1",
|
||||
"eu-west-3"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 0,
|
||||
"last_checked_at": null
|
||||
@@ -202,7 +235,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "012",
|
||||
"alias": "AWS Athena",
|
||||
"regions": ["us-east-1", "us-east-2"],
|
||||
"regions": [
|
||||
"us-east-1",
|
||||
"us-east-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 5,
|
||||
"last_checked_at": null
|
||||
@@ -219,7 +255,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "013",
|
||||
"alias": "AWS Certificate Manager",
|
||||
"regions": ["us-west-1", "us-west-2"],
|
||||
"regions": [
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 1,
|
||||
"last_checked_at": null
|
||||
@@ -236,7 +275,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "014",
|
||||
"alias": "AWS CloudFormation",
|
||||
"regions": ["eu-central-1", "eu-central-2"],
|
||||
"regions": [
|
||||
"eu-central-1",
|
||||
"eu-central-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 0,
|
||||
"last_checked_at": null
|
||||
@@ -253,7 +295,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "015",
|
||||
"alias": "AWS CloudTrail",
|
||||
"regions": ["us-east-1", "us-east-2"],
|
||||
"regions": [
|
||||
"us-east-1",
|
||||
"us-east-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 4,
|
||||
"last_checked_at": null
|
||||
@@ -270,7 +315,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "016",
|
||||
"alias": "AWS CloudWatch",
|
||||
"regions": ["us-west-1", "us-west-2"],
|
||||
"regions": [
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 2,
|
||||
"last_checked_at": null
|
||||
@@ -287,7 +335,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "017",
|
||||
"alias": "AWS Config",
|
||||
"regions": ["eu-west-1", "eu-west-3"],
|
||||
"regions": [
|
||||
"eu-west-1",
|
||||
"eu-west-3"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 0,
|
||||
"last_checked_at": null
|
||||
@@ -304,7 +355,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "018",
|
||||
"alias": "AWS Database Migration",
|
||||
"regions": ["us-east-1", "us-east-2"],
|
||||
"regions": [
|
||||
"us-east-1",
|
||||
"us-east-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 5,
|
||||
"last_checked_at": null
|
||||
@@ -321,7 +375,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "019",
|
||||
"alias": "AWS Glue",
|
||||
"regions": ["us-west-1", "us-west-2"],
|
||||
"regions": [
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 1,
|
||||
"last_checked_at": null
|
||||
@@ -338,7 +395,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "020",
|
||||
"alias": "AWS IAM",
|
||||
"regions": ["eu-central-1", "eu-central-2"],
|
||||
"regions": [
|
||||
"eu-central-1",
|
||||
"eu-central-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 0,
|
||||
"last_checked_at": null
|
||||
@@ -355,7 +415,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "021",
|
||||
"alias": "AWS Lambda",
|
||||
"regions": ["us-east-1", "us-east-2"],
|
||||
"regions": [
|
||||
"us-east-1",
|
||||
"us-east-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 4,
|
||||
"last_checked_at": null
|
||||
@@ -372,7 +435,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "022",
|
||||
"alias": "AWS Network Firewall",
|
||||
"regions": ["us-west-1", "us-west-2"],
|
||||
"regions": [
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 2,
|
||||
"last_checked_at": null
|
||||
@@ -389,7 +455,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "023",
|
||||
"alias": "AWS Organizations",
|
||||
"regions": ["eu-west-1", "eu-west-3"],
|
||||
"regions": [
|
||||
"eu-west-1",
|
||||
"eu-west-3"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 0,
|
||||
"last_checked_at": null
|
||||
@@ -406,7 +475,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "024",
|
||||
"alias": "AWS Resource Explorer",
|
||||
"regions": ["us-east-1", "us-east-2"],
|
||||
"regions": [
|
||||
"us-east-1",
|
||||
"us-east-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 3,
|
||||
"last_checked_at": null
|
||||
@@ -423,7 +495,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "025",
|
||||
"alias": "AWS Security Hub",
|
||||
"regions": ["us-west-1", "us-west-2"],
|
||||
"regions": [
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 5,
|
||||
"last_checked_at": null
|
||||
@@ -440,7 +515,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "026",
|
||||
"alias": "AWS Systems Manager",
|
||||
"regions": ["eu-west-1", "eu-west-3"],
|
||||
"regions": [
|
||||
"eu-west-1",
|
||||
"eu-west-3"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 0,
|
||||
"last_checked_at": null
|
||||
@@ -457,7 +535,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "027",
|
||||
"alias": "AWS Trusted Advisor",
|
||||
"regions": ["us-east-1", "us-east-2"],
|
||||
"regions": [
|
||||
"us-east-1",
|
||||
"us-east-2"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 2,
|
||||
"last_checked_at": null
|
||||
@@ -474,7 +555,10 @@
|
||||
"provider": "aws",
|
||||
"provider_id": "028",
|
||||
"alias": "IAM Access Analyzer",
|
||||
"regions": ["eu-west-1", "eu-west-3"],
|
||||
"regions": [
|
||||
"eu-west-1",
|
||||
"eu-west-3"
|
||||
],
|
||||
"findings": {
|
||||
"failed": 0,
|
||||
"last_checked_at": null
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 630 B |
@@ -6,13 +6,19 @@
|
||||
"incremental": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
@@ -24,7 +30,9 @@
|
||||
"strict": true,
|
||||
"target": "es5"
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
|
||||