Compare commits

...

41 Commits

Author SHA1 Message Date
HugoPBrito
147a772bf2 fix: try to match api and ui with previous branch 2024-12-12 13:34:01 +01:00
HugoPBrito
793cf78f3b feat: added testing 2024-12-11 14:22:56 +01:00
HugoPBrito
7d10727c34 feat: added check logic and metadata 2024-12-11 14:22:17 +01:00
HugoPBrito
0715c659c8 chore: restore 2024-12-03 16:30:43 +01:00
HugoPBrito
33bd651e19 chore: pull 2024-12-03 15:34:18 +01:00
HugoPBrito
5346867a6a fix: push 2024-12-03 13:20:23 +01:00
HugoPBrito
f174f8af7c fix: service tests 2024-12-03 13:15:53 +01:00
HugoPBrito
5205b85c49 feat: enhance logic 2024-12-03 13:02:49 +01:00
HugoPBrito
446b02e009 feat: add testing 2024-12-03 12:18:27 +01:00
HugoPBrito
c7ba87faba feat: enhance service logic 2024-12-03 12:02:02 +01:00
HugoPBrito
817ab62fe7 fix: log in providers authentication 2024-12-03 11:04:35 +01:00
HugoPBrito
359a53bf16 feat: added check logic and metadata 2024-12-03 10:54:31 +01:00
HugoPBrito
0ea3a202c4 chore: remove log print 2024-12-02 13:13:35 +01:00
HugoPBrito
0b8bf9851a chore: enhance format_rsa_key logic 2024-11-27 17:15:12 +01:00
HugoPBrito
27a8bb03d3 chore: enhanced logic of rsa format 2024-11-27 17:10:20 +01:00
HugoPBrito
6b7fa9ac4e feat: enhance print_credentials 2024-11-27 13:54:58 +01:00
HugoPBrito
b50ac49a7e feat: enhance authentication logic flow 2024-11-27 13:23:38 +01:00
HugoPBrito
93ea60cc86 feat: add testing for new auth methods 2024-11-26 17:18:57 +01:00
HugoPBrito
9fe50eb264 feat: resolved comment
Removed CIS compliance from check metadata
2024-11-26 14:21:15 +01:00
HugoPBrito
6f961139a1 feat: complete github app login 2024-11-26 14:10:02 +01:00
HugoPBrito
7001789997 feat: add github app login
Removed deprecated user-passwd login and replaced it with github app
2024-11-25 17:51:47 +01:00
HugoPBrito
aa0d82f500 feat: pat and oauth authentication with and without flags 2024-11-25 17:00:33 +01:00
HugoPBrito
b94a535aba feat: oauth login with flag 2024-11-25 16:05:11 +01:00
HugoPBrito
9c9526af80 fix: minor correction 2024-11-25 13:55:25 +01:00
HugoPBrito
1d0243652e feat: revert poetry lock 2024-11-25 13:51:01 +01:00
HugoPBrito
4e64a26ece feat: enhanced logic 2024-11-25 13:30:15 +01:00
HugoPBrito
b2e58c3045 feat: add default env PAT login
Added env PAT login by default and --pat flag retrieval
2024-11-25 13:07:52 +01:00
HugoPBrito
45668eb53a feat: added custom exceptions
Added custom exceptions for provider initialization like bad credentials or non existent env variables.
2024-11-25 11:06:07 +01:00
HugoPBrito
cac7c42f13 fix: logic of security.md retrieval 2024-11-22 11:48:14 +01:00
HugoPBrito
5ee960e13d feat: added testing
Added testing classes for provider, new service and check and corrected minor issues.
2024-11-20 18:09:10 +01:00
HugoPBrito
c11e6449f9 fix: reverted name 2024-11-20 11:42:31 +01:00
HugoPBrito
fa8a7f44de fix: reverted name 2024-11-20 11:33:38 +01:00
HugoPBrito
f028e41652 feat: renamed first check 2024-11-20 11:14:15 +01:00
MrCloudSec
b815eeb7ab Merge branch 'master' into github-poc 2024-11-19 12:35:50 -04:00
MrCloudSec
d0701ad0ce chore: revision 2024-11-19 12:34:49 -04:00
HugoPBrito
d4e5b7c7c7 feat: add new check to service 2024-11-19 17:14:34 +01:00
HugoPBrito
d27c5b7190 feat: first service implementation 2024-11-19 13:18:30 +01:00
MrCloudSec
5ce7aa33ad chore: revision 2024-11-18 13:15:46 -04:00
HugoPBrito
2628a4e506 fix: arguments parser 2024-11-18 15:04:12 +01:00
HugoPBrito
27b628152d feat: provider structural resemblance 2024-11-15 14:03:56 +01:00
HugoPBrito
f8f1e6c076 feat: basic PoC script
basic repo info retrieval
2024-11-12 15:03:36 +01:00
74 changed files with 55683 additions and 699 deletions

View File

@@ -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

View File

@@ -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 = {}

View File

@@ -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):

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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.")

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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"

View File

@@ -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:

View File

@@ -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:

View File

@@ -1,4 +1,4 @@
from unittest.mock import patch, call
from unittest.mock import call, patch
import pytest

View File

@@ -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:

View File

@@ -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,
)

View File

@@ -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:

View File

@@ -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

View File

@@ -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=["*"])

View File

@@ -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"])

View File

@@ -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"])

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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:

View File

View File

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

View File

@@ -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:
"""

View File

@@ -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

View File

@@ -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:
"""

View File

@@ -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:

View File

@@ -11,6 +11,7 @@ from prowler.config.config import (
orange_color,
)
from prowler.lib.logger import logger
from prowler.providers.github.models import GithubAppIdentityInfo, GithubIdentityInfo
def display_summary_table(
@@ -40,6 +41,13 @@ def display_summary_table(
elif provider.type == "kubernetes":
entity_type = "Context"
audited_entities = provider.identity.context
elif provider.type == "github":
if isinstance(provider.identity, GithubIdentityInfo):
entity_type = "User Name"
audited_entities = provider.identity.account_name
elif isinstance(provider.identity, GithubAppIdentityInfo):
entity_type = "App ID"
audited_entities = provider.identity.app_id
# Check if there are findings and that they are not all MANUAL
if findings and not all(finding.status == "MANUAL" for finding in findings):

View File

@@ -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(

View File

View File

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

View File

@@ -0,0 +1,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)

View 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."
)

View 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)),
)

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,30 @@
{
"Provider": "github",
"CheckID": "repository_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": ""
}

View File

@@ -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

View File

@@ -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": ""
}

View File

@@ -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

View File

@@ -0,0 +1,30 @@
{
"Provider": "github",
"CheckID": "repository_public_has_securitymd_file",
"CheckTitle": "Check if public repositories have a SECURITY.md file",
"CheckType": [],
"ServiceName": "repository",
"SubServiceName": "",
"ResourceIdTemplate": "github:user-id:repository/repository-name",
"Severity": "low",
"ResourceType": "Other",
"Description": "Ensure that public repositories have a SECURITY.md file.",
"Risk": "Not having a SECURITY.md file in a public repository may lead to security vulnerabilities being overlooked by users and contributors.",
"RelatedUrl": "https://docs.github.com/en/code-security/getting-started/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": ""
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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"
)

View File

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

View File

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

View File

@@ -0,0 +1,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."
)

View File

@@ -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})."
)

View File

@@ -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."
)

View 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

View File

@@ -17,4 +17,4 @@ build
!jest.config.js
!plopfile.js
!react-shim.js
!tsup.config.ts
!tsup.config.ts

View File

@@ -1 +1 @@
node_modules/
node_modules/

View File

@@ -6,5 +6,7 @@
"useTabs": false,
"semi": true,
"printWidth": 80,
"plugins": ["prettier-plugin-tailwindcss"]
"plugins": [
"prettier-plugin-tailwindcss"
]
}

View File

@@ -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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 547 KiB

After

Width:  |  Height:  |  Size: 547 KiB

View File

@@ -146,7 +146,7 @@ export const LaunchScanWorkflow = ({
</>
)}
</AnimatePresence>
{/*
{/*
<div className="flex flex-col justify-start">
<AnimatePresence>
{form.watch("providerId") && (

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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",