Repository: prowler-cloud/prowler
Pull request: feat(NHN): add NHN cloud provider with 6 checks
### Description

Added NHN cloud as a new provider with 6 checks.

**compute**
- check if instance has public IP
- check if instance has admin/root user as a login user
- check if instance only has default security_gorup

**network**
- check if vpc has empty routingtables
- check if vpc subnet has external router
- check if vpc subnet has dhcp enabled

More security checks still need to be implemented, but I’d appreciate feedback on whether I’m heading in the right direction.

### Checklist

- Are there new checks included in this PR? Yes / No
    - If so, do we need to update permissions for the provider? Please review this carefully.
- [x] Review if the code is being covered by tests.
- [ ] Review if code is being documented following this specification https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings
- [ ] Review if backport is needed.
- [ ] Review if is needed to change the [Readme.md](https://github.com/prowler-cloud/prowler/blob/master/README.md)

### License

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.


Problem statement:
Add NHN Cloud as a New Provider
### New Feature Motivation  
I’ve been testing Prowler and found it very useful. However, I noticed that NHN Cloud is not currently supported as a provider. Adding NHN Cloud support would be beneficial for users who rely on NHN Cloud for their cloud infrastructure security assessments.  

### Solution Proposed  
I propose adding NHN Cloud as a provider to Prowler, similar to existing cloud providers. This would include:  
- Implementing necessary integrations using NHN Cloud APIs  
- Adding security checks aligned with NHN Cloud best practices  
- Ensuring compatibility with Prowler CLI and Prowler App  

### Describe Alternatives You’ve Considered  
Currently, NHN Cloud users must perform security assessments manually or use other tools. Integrating NHN Cloud into Prowler would streamline security assessments and provide a unified experience.  

### Additional Context  
If the maintainers find this proposal valuable, I am willing to develop the feature and submit a PR. Let me know if you have any suggestions or requirements!

Hints from issue discussion:
Hey! @eeche Thanks for your feature request, we are adding more providers so we'll take your request into account.
Hello @eeche, are you related somehow with NHN Cloud? We are happy to start a conversation to see ways to collaborate. Please reach out to us directly via https://prowler.com/contact/ and talk further. Thank you!
Hello @toniblyx @pedrooot ! , I just opened a new PR (#6870). This is my first time contributing to Prowler, so I'm not sure if I'm doing it correctly. I'm working on adding NHN Cloud as a provider, and I would appreciate a review. If I'm on the right track, I plan to continue developing and adding security check features. Thank you!
That's great news @eeche 👏 we will review the PR soon!

Commit message: refactor(nhn): rename Check_Report_NHN to CheckReportNHN and update constructor for improved clarity

Patch to review:
diff --git a/README.md b/README.md
index c1b238fff5..d3a6bbacc5 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,7 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
 | Azure | 140 | 18 | 7 | 3 |
 | Kubernetes | 83 | 7 | 4 | 7 |
 | Microsoft365 | 5 | 2 | 1 | 0 |
+| NHN (Not Supported) | 6 | 2 | 0 | 0 |
 
 > You can list the checks, services, compliance frameworks and categories with `prowler <provider> --list-checks`, `prowler <provider> --list-services`, `prowler <provider> --list-compliance` and `prowler <provider> --list-categories`.
 
diff --git a/prowler/__main__.py b/prowler/__main__.py
index d27b7da8e9..666a97c337 100644
--- a/prowler/__main__.py
+++ b/prowler/__main__.py
@@ -52,6 +52,7 @@
 from prowler.lib.outputs.compliance.cis.cis_gcp import GCPCIS
 from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS
 from prowler.lib.outputs.compliance.cis.cis_microsoft365 import Microsoft365CIS
+from prowler.lib.outputs.compliance.cis.cis_nhn import NHNCIS
 from prowler.lib.outputs.compliance.compliance import display_compliance_table
 from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS
 from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS
@@ -85,6 +86,7 @@
 from prowler.providers.gcp.models import GCPOutputOptions
 from prowler.providers.kubernetes.models import KubernetesOutputOptions
 from prowler.providers.microsoft365.models import Microsoft365OutputOptions
+from prowler.providers.nhn.models import NHNOutputOptions
 
 
 def prowler():
@@ -270,6 +272,10 @@ def prowler():
         output_options = Microsoft365OutputOptions(
             args, bulk_checks_metadata, global_provider.identity
         )
+    elif provider == "nhn":
+        output_options = NHNOutputOptions(
+            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:
@@ -688,6 +694,36 @@ def prowler():
                 generated_outputs["compliance"].append(generic_compliance)
                 generic_compliance.batch_write_data_to_file()
 
+    elif provider == "nhn":
+        for compliance_name in input_compliance_frameworks:
+            if compliance_name.startswith("cis_"):
+                # Generate CIS Finding Object
+                filename = (
+                    f"{output_options.output_directory}/compliance/"
+                    f"{output_options.output_filename}_{compliance_name}.csv"
+                )
+                cis = NHNCIS(
+                    findings=finding_outputs,
+                    compliance=bulk_compliance_frameworks[compliance_name],
+                    create_file_descriptor=True,
+                    file_path=filename,
+                )
+                generated_outputs["compliance"].append(cis)
+                cis.batch_write_data_to_file()
+            else:
+                filename = (
+                    f"{output_options.output_directory}/compliance/"
+                    f"{output_options.output_filename}_{compliance_name}.csv"
+                )
+                generic_compliance = GenericCompliance(
+                    findings=finding_outputs,
+                    compliance=bulk_compliance_frameworks[compliance_name],
+                    create_file_descriptor=True,
+                    file_path=filename,
+                )
+                generated_outputs["compliance"].append(generic_compliance)
+                generic_compliance.batch_write_data_to_file()
+
     # AWS Security Hub Integration
     if provider == "aws":
         # Send output to S3 if needed (-B / -D) for all the output formats
diff --git a/prowler/compliance/nhn/__init__.py b/prowler/compliance/nhn/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/config/config.py b/prowler/config/config.py
index 9c5a4021a6..6089a24514 100644
--- a/prowler/config/config.py
+++ b/prowler/config/config.py
@@ -29,6 +29,7 @@ class Provider(str, Enum):
     AZURE = "azure"
     KUBERNETES = "kubernetes"
     MICROSOFT365 = "microsoft365"
+    NHN = "nhn"
 
 
 # Compliance
diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py
index 7dba878217..9c1947313d 100644
--- a/prowler/lib/check/models.py
+++ b/prowler/lib/check/models.py
@@ -573,6 +573,37 @@ def __init__(
         self.location = resource_location
 
 
+@dataclass
+class CheckReportNHN(Check_Report):
+    """Contains the NHN Check's finding information."""
+
+    resource_name: str
+    resource_id: str
+    location: str
+
+    def __init__(
+        self,
+        metadata: dict,
+        resource: any,
+        resource_name: str,
+        resource_id: str,
+        resource_location: str = "kr1",
+    ) -> None:
+        """Initialize the NHN Check's finding information.
+
+        Args:
+            metadata: The metadata of the check.
+            resource: Basic information about the resource. Defaults to None.
+            resource_name: The name of the resource related with the finding.
+            resource_id: The id of the resource related with the finding.
+            resource_location: The location of the resource related with the finding.
+        """
+        super().__init__(metadata, resource)
+        self.resource_name = resource_name
+        self.resource_id = resource_id
+        self.location = resource_location
+
+
 # Testing Pending
 def load_check_metadata(metadata_file: str) -> CheckMetadata:
     """
diff --git a/prowler/lib/cli/parser.py b/prowler/lib/cli/parser.py
index cff8940134..fd262638cf 100644
--- a/prowler/lib/cli/parser.py
+++ b/prowler/lib/cli/parser.py
@@ -26,15 +26,16 @@ def __init__(self):
         self.parser = argparse.ArgumentParser(
             prog="prowler",
             formatter_class=RawTextHelpFormatter,
-            usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,microsoft365,dashboard} ...",
+            usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,microsoft365,nhn,dashboard} ...",
             epilog="""
 Available Cloud Providers:
-  {aws,azure,gcp,kubernetes}
+  {aws,azure,gcp,kubernetes,microsoft365,nhn}
     aws                 AWS Provider
     azure               Azure Provider
     gcp                 GCP Provider
     kubernetes          Kubernetes Provider
     microsoft365        Microsoft 365 Provider
+    nhn                 NHN Cloud Platform Provider (Not Supported)
 
 Available components:
     dashboard           Local dashboard
diff --git a/prowler/lib/outputs/compliance/cis/cis_nhn.py b/prowler/lib/outputs/compliance/cis/cis_nhn.py
new file mode 100644
index 0000000000..ce476c7fae
--- /dev/null
+++ b/prowler/lib/outputs/compliance/cis/cis_nhn.py
@@ -0,0 +1,79 @@
+from prowler.lib.check.compliance_models import Compliance
+from prowler.lib.outputs.compliance.cis.models import NHNCISModel
+from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
+from prowler.lib.outputs.finding import Finding
+
+
+class NHNCIS(ComplianceOutput):
+    """
+    This class represents the NHN CIS compliance output.
+
+    Attributes:
+        - _data (list): A list to store transformed data from findings.
+        - _file_descriptor (TextIOWrapper): A file descriptor to write data to a file.
+
+    Methods:
+        - transform: Transforms findings into NHN CIS compliance format.
+    """
+
+    def transform(
+        self,
+        findings: list[Finding],
+        compliance: Compliance,
+        compliance_name: str,
+    ) -> None:
+        """
+        Transforms a list of findings into NHN CIS compliance format.
+
+        Parameters:
+            - findings (list): A list of findings.
+            - compliance (Compliance): A compliance model.
+            - compliance_name (str): The name of the compliance model.
+
+        Returns:
+            - None
+        """
+        for finding in findings:
+            # Get the compliance requirements for the finding
+            finding_requirements = finding.compliance.get(compliance_name, [])
+            for requirement in compliance.Requirements:
+                if requirement.Id in finding_requirements:
+                    for attribute in requirement.Attributes:
+                        compliance_row = NHNCISModel(
+                            Provider=finding.provider,
+                            Description=compliance.Description,
+                            SubscriptionId=finding.account_uid,
+                            Location=finding.region,
+                            AssessmentDate=str(finding.timestamp),
+                            Requirements_Id=requirement.Id,
+                            Requirements_Description=requirement.Description,
+                            Requirements_Attributes_Section=attribute.Section,
+                            Status=finding.status,
+                            StatusExtended=finding.status_extended,
+                            ResourceId=finding.resource_uid,
+                            ResourceName=finding.resource_name,
+                            CheckId=finding.check_id,
+                            Muted=finding.muted,
+                        )
+                        self._data.append(compliance_row)
+        # Add manual requirements to the compliance output
+        for requirement in compliance.Requirements:
+            if not requirement.Checks:
+                for attribute in requirement.Attributes:
+                    compliance_row = NHNCISModel(
+                        Provider=compliance.Provider.lower(),
+                        Description=compliance.Description,
+                        SubscriptionId="",
+                        Location="",
+                        AssessmentDate=str(finding.timestamp) if findings else "",
+                        Requirements_Id=requirement.Id,
+                        Requirements_Description=requirement.Description,
+                        Requirements_Attributes_Section=attribute.Section,
+                        Status="MANUAL",
+                        StatusExtended="Manual check",
+                        ResourceId="manual_check",
+                        ResourceName="Manual check",
+                        CheckId="manual",
+                        Muted=False,
+                    )
+                    self._data.append(compliance_row)
diff --git a/prowler/lib/outputs/compliance/cis/models.py b/prowler/lib/outputs/compliance/cis/models.py
index 1f6e661655..08587ac786 100644
--- a/prowler/lib/outputs/compliance/cis/models.py
+++ b/prowler/lib/outputs/compliance/cis/models.py
@@ -163,6 +163,37 @@ class KubernetesCISModel(BaseModel):
     Muted: bool
 
 
+class NHNCISModel(BaseModel):
+    """
+    NHNCISModel generates a finding's output in NHN CIS Compliance format.
+    """
+
+    Provider: str
+    Description: str
+    SubscriptionId: str
+    Location: str
+    AssessmentDate: str
+    Requirements_Id: str
+    Requirements_Description: str
+    Requirements_Attributes_Section: str
+    Requirements_Attributes_Profile: str
+    Requirements_Attributes_AssessmentStatus: str
+    Requirements_Attributes_Description: str
+    Requirements_Attributes_RationaleStatement: str
+    Requirements_Attributes_ImpactStatement: str
+    Requirements_Attributes_RemediationProcedure: str
+    Requirements_Attributes_AuditProcedure: str
+    Requirements_Attributes_AdditionalInformation: str
+    Requirements_Attributes_DefaultValue: str
+    Requirements_Attributes_References: str
+    Status: str
+    StatusExtended: str
+    ResourceId: str
+    ResourceName: str
+    CheckId: str
+    Muted: bool
+
+
 # TODO: Create a parent class for the common fields of CIS and have the specific classes from each provider to inherit from it.
 # It is not done yet because it is needed to respect the current order of the fields in the output file.
 
diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py
index f41ed6d935..9e9ff49d19 100644
--- a/prowler/lib/outputs/finding.py
+++ b/prowler/lib/outputs/finding.py
@@ -256,6 +256,20 @@ def generate_output(
                 output_data["resource_uid"] = check_output.resource_id
                 output_data["region"] = check_output.location
 
+            elif provider.type == "nhn":
+                output_data["auth_method"] = (
+                    f"{provider.identity.identity_type}: {provider.identity.identity_id}"
+                )
+                output_data["account_uid"] = get_nested_attribute(
+                    provider, "identity.tenant_id"
+                )
+                output_data["account_name"] = get_nested_attribute(
+                    provider, "identity.tenant_domain"
+                )
+                output_data["resource_name"] = check_output.resource_name
+                output_data["resource_uid"] = check_output.resource_id
+                output_data["region"] = check_output.location
+
             # check_output Unique ID
             # TODO: move this to a function
             # TODO: in Azure, GCP and K8s there are fidings without resource_name
diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py
index de99168326..8e3c88efd6 100644
--- a/prowler/lib/outputs/html/html.py
+++ b/prowler/lib/outputs/html/html.py
@@ -591,6 +591,51 @@ def get_microsoft365_assessment_summary(provider: Provider) -> str:
             )
             return ""
 
+    def get_nhn_assessment_summary(provider: Provider) -> str:
+        """
+        get_nhn_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">
+                            NHN Assessment Summary
+                        </div>
+                        <ul class="list-group list-group-flush">
+                            <li class="list-group-item">
+                                <b>NHN Tenant Domain:</b> {provider.identity.tenant_domain}
+                            </li>
+                        </ul>
+                    </div>
+                </div>
+                <div class="col-md-4">
+                <div class="card">
+                    <div class="card-header">
+                        NHN Credentials
+                    </div>
+                    <ul class="list-group list-group-flush">
+                        <li class="list-group-item">
+                            <b>NHN Identity Type:</b> {provider.identity.identity_type}
+                            </li>
+                            <li class="list-group-item">
+                                <b>NHN Identity ID:</b> {provider.identity.identity_id}
+                            </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:
         """
diff --git a/prowler/lib/outputs/outputs.py b/prowler/lib/outputs/outputs.py
index dfd37429dd..0912f986af 100644
--- a/prowler/lib/outputs/outputs.py
+++ b/prowler/lib/outputs/outputs.py
@@ -18,6 +18,8 @@ def stdout_report(finding, color, verbose, status, fix):
         details = finding.namespace.lower()
     if finding.check_metadata.Provider == "microsoft365":
         details = finding.location
+    if finding.check_metadata.Provider == "nhn":
+        details = finding.location
 
     if (verbose or fix) and (not status or finding.status in status):
         if finding.muted:
diff --git a/prowler/lib/outputs/summary_table.py b/prowler/lib/outputs/summary_table.py
index 63d4480f80..b860285e2e 100644
--- a/prowler/lib/outputs/summary_table.py
+++ b/prowler/lib/outputs/summary_table.py
@@ -43,6 +43,9 @@ def display_summary_table(
         elif provider.type == "microsoft365":
             entity_type = "Tenant Domain"
             audited_entities = provider.identity.tenant_domain
+        elif provider.type == "nhn":
+            entity_type = "Tenant Domain"
+            audited_entities = provider.identity.tenant_domain
 
         # Check if there are findings and that they are not all MANUAL
         if findings and not all(finding.status == "MANUAL" for finding in findings):
diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py
index ec9c5de0db..b8fe46d9e8 100644
--- a/prowler/providers/common/provider.py
+++ b/prowler/providers/common/provider.py
@@ -222,6 +222,15 @@ def init_global_provider(arguments: Namespace) -> None:
                         tenant_id=arguments.tenant_id,
                         fixer_config=fixer_config,
                     )
+                elif "nhn" in provider_class_name.lower():
+                    provider_class(
+                        username=arguments.nhn_username,
+                        password=arguments.nhn_password,
+                        tenant_id=arguments.nhn_tenant_id,
+                        config_path=arguments.config_file,
+                        mutelist_path=arguments.mutelist_file,
+                        fixer_config=fixer_config,
+                    )
 
         except TypeError as error:
             logger.critical(
diff --git a/prowler/providers/nhn/__init__.py b/prowler/providers/nhn/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/nhn/exceptions/exceptions.py b/prowler/providers/nhn/exceptions/exceptions.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/nhn/lib/__init__.py b/prowler/providers/nhn/lib/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/nhn/lib/arguments/__init__.py b/prowler/providers/nhn/lib/arguments/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/nhn/lib/arguments/arguments.py b/prowler/providers/nhn/lib/arguments/arguments.py
new file mode 100644
index 0000000000..d4925090e6
--- /dev/null
+++ b/prowler/providers/nhn/lib/arguments/arguments.py
@@ -0,0 +1,17 @@
+def init_parser(self):
+    """Init the NHN Provider CLI parser"""
+    nhn_parser = self.subparsers.add_parser(
+        "nhn", parents=[self.common_providers_parser], help="NHN Provider"
+    )
+
+    # Authentication
+    nhn_auth_subparser = nhn_parser.add_argument_group("Authentication")
+    nhn_auth_subparser.add_argument(
+        "--nhn-username", nargs="?", default=None, help="NHN API Username"
+    )
+    nhn_auth_subparser.add_argument(
+        "--nhn-password", nargs="?", default=None, help="NHN API Password"
+    )
+    nhn_auth_subparser.add_argument(
+        "--nhn-tenant-id", nargs="?", default=None, help="NHN Tenant ID"
+    )
diff --git a/prowler/providers/nhn/lib/mutelist/__init__.py b/prowler/providers/nhn/lib/mutelist/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/nhn/lib/mutelist/mutelist.py b/prowler/providers/nhn/lib/mutelist/mutelist.py
new file mode 100644
index 0000000000..14144d00f8
--- /dev/null
+++ b/prowler/providers/nhn/lib/mutelist/mutelist.py
@@ -0,0 +1,14 @@
+from prowler.lib.check.models import CheckReportNHN
+from prowler.lib.mutelist.mutelist import Mutelist
+from prowler.lib.outputs.utils import unroll_dict, unroll_tags
+
+
+class NHNMutelist(Mutelist):
+    def is_finding_muted(self, finding: CheckReportNHN) -> bool:
+        return self.is_muted(
+            finding.resource_id,
+            finding.check_metadata.CheckID,
+            finding.location,
+            finding.resource_name,
+            unroll_dict(unroll_tags(finding.resource_tags)),
+        )
diff --git a/prowler/providers/nhn/lib/service/__init__.py b/prowler/providers/nhn/lib/service/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/nhn/lib/service/service.py b/prowler/providers/nhn/lib/service/service.py
new file mode 100644
index 0000000000..36781b8895
--- /dev/null
+++ b/prowler/providers/nhn/lib/service/service.py
@@ -0,0 +1 @@
+# TODO: If more services are added, we need to add common methods here
diff --git a/prowler/providers/nhn/models.py b/prowler/providers/nhn/models.py
new file mode 100644
index 0000000000..9c5ec5ced9
--- /dev/null
+++ b/prowler/providers/nhn/models.py
@@ -0,0 +1,58 @@
+from pydantic import BaseModel
+
+from prowler.config.config import output_file_timestamp
+from prowler.providers.common.models import ProviderOutputOptions
+
+
+class NHNIdentityInfo(BaseModel):
+    """
+    NHNIdentityInfo holds basic identity fields for the NHN provider.
+
+    Attributes:
+        - identity_id (str): An optional identity ID if used by NHN services.
+        - identity_type (str): The type or role of the identity, if needed.
+        - tenant_domain (str): The tenant domain if applicable.
+          (Some NHN services might require a domain or project domain.)
+        - tenant_id (str): The tenant ID for the NHN Cloud account.
+        - username (str): The username associated with the account.
+        - auth_method (str): The authentication method used to access the account.
+    """
+
+    identity_id: str = ""
+    identity_type: str = ""
+    tenant_domain: str = ""
+    tenant_id: str
+    username: str
+    auth_method: str
+
+
+class NHNOutputOptions(ProviderOutputOptions):
+    """
+    NHNOutputOptions overrides ProviderOutputOptions for NHN-specific output logic.
+    For example, generating a filename that includes the NHN tenant_id.
+
+    Attributes inherited from ProviderOutputOptions:
+        - output_filename (str): The base filename used for generated reports.
+        - output_directory (str): The directory to store the output files.
+        - ... see ProviderOutputOptions for more details.
+
+    Methods:
+        - __init__: Customizes the output filename logic for NHN.
+    """
+
+    def __init__(self, arguments, bulk_checks_metadata, identity: NHNIdentityInfo):
+        super().__init__(arguments, bulk_checks_metadata)
+
+        # If --output-filename is not specified, build a default name.
+        if not getattr(arguments, "output_filename", None):
+            # If tenant_id exists, include it in the filename (e.g., prowler-output-nhn-<tenant_id>-20230101)
+            if identity.tenant_id:
+                self.output_filename = (
+                    f"prowler-output-nhn-{identity.tenant_id}-{output_file_timestamp}"
+                )
+            # Otherwise just 'prowler-output-nhn-<timestamp>'
+            else:
+                self.output_filename = f"prowler-output-nhn-{output_file_timestamp}"
+        # If --output-filename was explicitly given, respect that
+        else:
+            self.output_filename = arguments.output_filename
diff --git a/prowler/providers/nhn/nhn_provider.py b/prowler/providers/nhn/nhn_provider.py
new file mode 100644
index 0000000000..72210ace81
--- /dev/null
+++ b/prowler/providers/nhn/nhn_provider.py
@@ -0,0 +1,309 @@
+import os
+from typing import Optional
+
+import requests
+from colorama import Style
+
+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.utils.utils import print_boxes
+from prowler.providers.common.models import Audit_Metadata, Connection
+from prowler.providers.common.provider import Provider
+from prowler.providers.nhn.lib.mutelist.mutelist import NHNMutelist
+from prowler.providers.nhn.models import NHNIdentityInfo
+
+
+class NhnProvider(Provider):
+    """
+    NHN Provider class to handle the NHN provider
+
+    Attributes:
+    - _type: str -> The type of the provider, which is set to "nhn".
+    - _session: requests.Session -> The session object associated with the NHN provider.
+    - _identity: NHNIdentityInfo -> The identity information for the NHN provider.
+    - _audit_config: dict -> The audit configuration for the NHN provider.
+    - _mutelist: NHNMutelist -> The mutelist object associated with the NHN provider.
+    - audit_metadata: Audit_Metadata -> The audit metadata for the NHN provider.
+
+    Methods:
+    - __init__: Initializes the NHN provider.
+    - type: Returns the type of the NHN provider.
+    - identity: Returns the identity of the NHN provider.(ex: tenant_id, username)
+    - session: Returns the session object associated with the NHN provider.(ex: Bearer token)
+    - audit_config: Returns the audit configuration for the NHN provider.
+    - fixer_config: Returns the fixer configuration.
+    - mutelist: Returns the mutelist object associated with the NHN provider.
+    - validate_arguments: Validates the NHN provider arguments.(ex: username, password, tenant_id)
+    - print_credentials: Prints the NHN credentials information.(ex: username, tenant_id)
+    - setup_session: Set up the NHN session with the specified authentication method.
+    - test_connection: tests the provider connection
+    """
+
+    _type: str = "nhn"
+    _session: Optional[requests.Session]
+    _identity: NHNIdentityInfo
+    _audit_config: dict
+    _mutelist: NHNMutelist
+    # TODO: this is not optional, enforce for all providers
+    audit_metadata: Audit_Metadata
+
+    def __init__(
+        self,
+        username: str = None,
+        password: str = None,
+        tenant_id: str = None,
+        config_path: str = None,
+        fixer_config: dict = None,
+        mutelist_path: str = None,
+        mutelist_content: dict = None,
+    ):
+        """
+        Initializes the NHN provider.
+
+        Args:
+            - username: The NHN Cloud client ID
+            - password: The NHN Cloud client password
+            - tenant_id: The NHN Cloud Tenant ID
+            - config_path: The path to the configuration file.
+            - fixer_config: The fixer configuration.
+            - mutelist_path: The path to the mutelist file.
+            - mutelist_content: The mutelist content.
+        """
+        logger.info("Initializing Nhn Provider...")
+
+        # 1) Store argument values
+        self._username = username or os.getenv("NHN_USERNAME")
+        self._password = password or os.getenv("NHN_PASSWORD")
+        self._tenant_id = tenant_id or os.getenv("NHN_TENANT_ID")
+
+        if not all([self._username, self._password, self._tenant_id]):
+            raise ValueError("NhnProvider requires username, password and tenant_id")
+
+        # 2) Load audit_config, fixer_config, mutelist
+        self._fixer_config = fixer_config if fixer_config else {}
+        if not config_path:
+            config_path = default_config_file_path
+        self._audit_config = load_and_validate_config_file(self._type, config_path)
+
+        if mutelist_content:
+            self._mutelist = NHNMutelist(mutelist_content=mutelist_content)
+        else:
+            if not mutelist_path:
+                mutelist_path = get_default_mute_file_path(self._type)
+            self._mutelist = NHNMutelist(mutelist_path=mutelist_path)
+
+        # 3) Initialize session/token
+        self._token = None
+        self._session = None
+        self.setup_session()
+
+        # 4) Create NHNIdentityInfo object
+        self._identity = NHNIdentityInfo(
+            tenant_id=self._tenant_id,
+            username=self._username,
+            auth_method="password",
+        )
+
+        Provider.set_global_provider(self)
+
+    @property
+    def type(self) -> str:
+        """
+        Returns the type of the provider ("nhn").
+        """
+        return self._type
+
+    @property
+    def identity(self) -> str:
+        """
+        Returns the NHNIdentityInfo object, which may contain tenant_id, username, etc.
+        """
+        return self._identity
+
+    @property
+    def session(self) -> str:
+        """
+        Returns the requests.Session object for NHN API calls.
+        """
+        return self._session
+
+    @property
+    def audit_config(self) -> dict:
+        """
+        Returns the audit configuration loaded from file or default settings.
+        """
+        return self._audit_config
+
+    @property
+    def fixer_config(self) -> dict:
+        """
+        Returns any fixer configuration provided to the NHN provider.
+        """
+        return self._fixer_config
+
+    @property
+    def mutelist(self) -> dict:
+        """
+        Returns the NHNMutelist object for handling any muted checks.
+        """
+        return self._mutelist
+
+    @staticmethod
+    def validate_arguments(username: str, password: str, tenant_id: str) -> None:
+        """
+        Ensures that username, password, and tenant_id are not empty.
+        """
+        if not username or not password or not tenant_id:
+            raise ValueError("NHN Provider requires username, password and tenant_id.")
+
+    def print_credentials(self) -> None:
+        """
+        Prints the NHN credentials in a simple box format.
+        """
+        report_lines = [
+            f"  Username: {self._username}",
+            f"  TenantID: {self._tenant_id}",
+        ]
+        report_title = (
+            f"{Style.BRIGHT}Using the NHN credentials below:{Style.RESET_ALL}"
+        )
+        print_boxes(report_lines, report_title)
+
+    def setup_session(self) -> None:
+        """
+        Implement NHN Cloud Authentication method by calling Keystone v2.0 API(POST /v2.0/tokens).
+        ex) https://api-identity-infrastructure.nhncloudservice.com/v2.0/tokens
+        {
+            "auth": {
+                "tenantId": "f5073eaa26b64cffbee89411df94ce01",
+                "passwordCredentials": {
+                    "username": "user@example.com",
+                    "password": "secretsecret"
+                }
+            }
+        }
+
+        On success, it creates a requests.Session and sets the X-Auth-Token header.
+        """
+        url = "https://api-identity-infrastructure.nhncloudservice.com/v2.0/tokens"
+        data = {
+            "auth": {
+                "tenantId": self._tenant_id,
+                "passwordCredentials": {
+                    "username": self._username,
+                    "password": self._password,
+                },
+            }
+        }
+        try:
+            response = requests.post(url, json=data, timeout=10)
+            if response.status_code == 200:
+                resp_json = response.json()
+                self._token = resp_json["access"]["token"]["id"]
+                sess = requests.Session()
+                sess.headers.update(
+                    {"X-Auth-Token": self._token, "Content-Type": "application/json"}
+                )
+                self._session = sess
+                logger.info("NHN token acquired successfully and session is set up.")
+            else:
+                logger.critical(
+                    f"Failed to get token. Status: {response.status_code}, Body: {response.text}"
+                )
+                raise ValueError("Failed to get NHN token")
+        except Exception as e:
+            logger.critical(f"[setup_session] Error: {e}")
+            raise e
+
+    @staticmethod
+    def test_connection(
+        username: str,
+        password: str,
+        tenant_id: str,
+        raise_on_exception: bool = True,
+    ) -> Connection:
+        """
+        Test connection to NHN Cloud by performing:
+          1) Keystone token request
+          2) (Optional) a small test API call to confirm credentials are valid
+
+        Args:
+            username (str): NHN Cloud user ID (email)
+            password (str): NHN Cloud user password
+            tenant_id (str): NHN Cloud tenant ID
+            raise_on_exception (bool): If True, raise the caught exception;
+                                       if False, return Connection(error=exception).
+
+        Returns:
+            Connection:
+                Connection(is_connected=True) if success,
+                otherwise Connection(error=Exception or custom error).
+        """
+        try:
+            # 1) Validate arguments (예: username/password/tenant_id)
+            if not username or not password or not tenant_id:
+                error_msg = (
+                    "NHN test_connection error: missing username/password/tenant_id"
+                )
+                logger.error(error_msg)
+                raise ValueError(error_msg)
+
+            # 2) Request Keystone token
+            token_url = (
+                "https://api-identity-infrastructure.nhncloudservice.com/v2.0/tokens"
+            )
+            data = {
+                "auth": {
+                    "tenantId": tenant_id,
+                    "passwordCredentials": {
+                        "username": username,
+                        "password": password,
+                    },
+                }
+            }
+            resp = requests.post(token_url, json=data, timeout=10)
+            if resp.status_code != 200:
+                # Fail
+                error_msg = f"Failed to get token. Status: {resp.status_code}, Body: {resp.text}"
+                logger.error(error_msg)
+                if raise_on_exception:
+                    raise Exception(error_msg)
+                return Connection(error=Exception(error_msg))
+
+            # Success
+            token_json = resp.json()
+            keystone_token = token_json["access"]["token"]["id"]
+            logger.info("NHN test_connection: Successfully acquired Keystone token.")
+
+            # 3) (Optional) Test API call to confirm credentials are valid
+            compute_endpoint = f"https://kr1-api-instance.infrastructure.cloud.toast.com/v2/{tenant_id}"
+
+            # Check servers list
+            headers = {
+                "X-Auth-Token": keystone_token,
+                "Content-Type": "application/json",
+            }
+            servers_resp = requests.get(
+                f"{compute_endpoint}/servers", headers=headers, timeout=10
+            )
+            if servers_resp.status_code == 200:
+                logger.info(
+                    "NHN test_connection: /servers call success. Credentials valid."
+                )
+                return Connection(is_connected=True)
+            else:
+                error_msg = f"/servers call failed. Status: {servers_resp.status_code}, Body: {servers_resp.text}"
+                logger.error(error_msg)
+                if raise_on_exception:
+                    raise Exception(error_msg)
+                return Connection(error=Exception(error_msg))
+
+        except Exception as e:
+            logger.critical(f"{e.__class__.__name__}[{e.__traceback__.tb_lineno}]: {e}")
+            if raise_on_exception:
+                raise e
+            return Connection(error=e)
diff --git a/prowler/providers/nhn/services/compute/__init__.py b/prowler/providers/nhn/services/compute/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/nhn/services/compute/compute_client.py b/prowler/providers/nhn/services/compute/compute_client.py
new file mode 100644
index 0000000000..cf4bc98df0
--- /dev/null
+++ b/prowler/providers/nhn/services/compute/compute_client.py
@@ -0,0 +1,4 @@
+from prowler.providers.common.provider import Provider
+from prowler.providers.nhn.services.compute.compute_service import NHNComputeService
+
+compute_client = NHNComputeService(Provider.get_global_provider())
diff --git a/prowler/providers/nhn/services/compute/compute_instance_login_user/__init__.py b/prowler/providers/nhn/services/compute/compute_instance_login_user/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/nhn/services/compute/compute_instance_login_user/compute_instance_login_user.metadata.json b/prowler/providers/nhn/services/compute/compute_instance_login_user/compute_instance_login_user.metadata.json
new file mode 100644
index 0000000000..9d8fa9c43b
--- /dev/null
+++ b/prowler/providers/nhn/services/compute/compute_instance_login_user/compute_instance_login_user.metadata.json
@@ -0,0 +1,33 @@
+{
+    "Provider": "nhn",
+    "CheckID": "compute_instance_login_user",
+    "CheckTitle": "Check for Administrative Login Users in NHN Compute Instances",
+    "CheckType": [], 
+    "ServiceName": "compute",
+    "SubServiceName": "",
+    "ResourceIdTemplate": "",
+    "Risk": "",
+    "Severity": "high",
+    "ResourceType": "VMInstance",
+    "Description": "Checks if NHN Compute instances have administrative login users.",
+    "Remediation": {
+        "Code": {
+            "CLI": "",
+            "NativeIaC": "",
+            "Other": "",
+            "Terraform": ""
+          },
+        "Recommendation": {
+        "Text": "Review the login users configured for each VM instance.", 
+        "Url": ""
+        }
+    },
+    "Categories": [],
+    "DependsOn": [],
+    "RelatedTo": [],
+    "Notes": "",
+    "RelatedUrl": "",
+    "Enabled": true,
+    "Compliances": []
+  }
+  
\ No newline at end of file
diff --git a/prowler/providers/nhn/services/compute/compute_instance_login_user/compute_instance_login_user.py b/prowler/providers/nhn/services/compute/compute_instance_login_user/compute_instance_login_user.py
new file mode 100644
index 0000000000..ffcadf6dd9
--- /dev/null
+++ b/prowler/providers/nhn/services/compute/compute_instance_login_user/compute_instance_login_user.py
@@ -0,0 +1,25 @@
+from prowler.lib.check.models import Check, CheckReportNHN
+from prowler.providers.nhn.services.compute.compute_client import compute_client
+
+
+class compute_instance_login_user(Check):
+    def execute(self):
+        findings = []
+        for instance in compute_client.instances:
+            report = CheckReportNHN(
+                metadata=self.metadata(),
+                resource=instance,
+                resource_name=instance.name,
+                resource_id=instance.id,
+                resource_location="kr1",
+            )
+            report.status = "PASS"
+            report.status_extended = (
+                f"VM Instance {instance.name} has a appropriate login user."
+            )
+            if instance.login_user:
+                report.status = "FAIL"
+                report.status_extended = f"VM Instance {instance.name} has an Administrative(admin/root) login user."
+            findings.append(report)
+
+        return findings
diff --git a/prowler/providers/nhn/services/compute/compute_instance_public_ip/__init__.py b/prowler/providers/nhn/services/compute/compute_instance_public_ip/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/nhn/services/compute/compute_instance_public_ip/compute_instance_public_ip.metadata.json b/prowler/providers/nhn/services/compute/compute_instance_public_ip/compute_instance_public_ip.metadata.json
new file mode 100644
index 0000000000..81ff983321
--- /dev/null
+++ b/prowler/providers/nhn/services/compute/compute_instance_public_ip/compute_instance_public_ip.metadata.json
@@ -0,0 +1,33 @@
+{
+    "Provider": "nhn",
+    "CheckID": "compute_instance_public_ip",
+    "CheckTitle": "Check for Public IP in NHN Compute Instances",
+    "CheckType": [], 
+    "ServiceName": "compute",
+    "SubServiceName": "",
+    "ResourceIdTemplate": "",
+    "Risk": "",
+    "Severity": "low",
+    "ResourceType": "VMInstance",
+    "Description": "Check if a floating (public) IP is assigned to an NHN compute instance.",
+    "Remediation": {
+        "Code": {
+            "CLI": "",
+            "NativeIaC": "",
+            "Other": "",
+            "Terraform": ""
+          },
+        "Recommendation": {
+        "Text": "Remove or unassign floating IP if not required to reduce external exposure.", 
+        "Url": ""
+        }
+    },
+    "Categories": [],
+    "DependsOn": [],
+    "RelatedTo": [],
+    "Notes": "",
+    "RelatedUrl": "",
+    "Enabled": true,
+    "Compliances": []
+  }
+  
\ No newline at end of file
diff --git a/prowler/providers/nhn/services/compute/compute_instance_public_ip/compute_instance_public_ip.py b/prowler/providers/nhn/services/compute/compute_instance_public_ip/compute_instance_public_ip.py
new file mode 100644
index 0000000000..ed560514bd
--- /dev/null
+++ b/prowler/providers/nhn/services/compute/compute_instance_public_ip/compute_instance_public_ip.py
@@ -0,0 +1,25 @@
+from prowler.lib.check.models import Check, CheckReportNHN
+from prowler.providers.nhn.services.compute.compute_client import compute_client
+
+
+class compute_instance_public_ip(Check):
+    def execute(self):
+        findings = []
+        for instance in compute_client.instances:
+            report = CheckReportNHN(
+                metadata=self.metadata(),
+                resource=instance,
+                resource_name=instance.name,
+                resource_id=instance.id,
+                resource_location="kr1",
+            )
+            report.status = "PASS"
+            report.status_extended = (
+                f"VM Instance {instance.name} does not have a public IP."
+            )
+            if instance.public_ip:
+                report.status = "FAIL"
+                report.status_extended = f"VM Instance {instance.name} has a public IP."
+            findings.append(report)
+
+        return findings
diff --git a/prowler/providers/nhn/services/compute/compute_instance_security_groups/__init__.py b/prowler/providers/nhn/services/compute/compute_instance_security_groups/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/nhn/services/compute/compute_instance_security_groups/compute_instance_security_groups.metadata.json b/prowler/providers/nhn/services/compute/compute_instance_security_groups/compute_instance_security_groups.metadata.json
new file mode 100644
index 0000000000..d0f6786052
--- /dev/null
+++ b/prowler/providers/nhn/services/compute/compute_instance_security_groups/compute_instance_security_groups.metadata.json
@@ -0,0 +1,33 @@
+{
+    "Provider": "nhn",
+    "CheckID": "compute_instance_security_groups",
+    "CheckTitle": "Check NHN Compute Security Group Configuration",
+    "CheckType": [], 
+    "ServiceName": "compute",
+    "SubServiceName": "",
+    "ResourceIdTemplate": "",
+    "Risk": "",
+    "Severity": "medium",
+    "ResourceType": "VMInstance",
+    "Description": "Checks if NHN Compute VM instances are using appropriate security group configurations. Using only the default security group can pose a security risk.",
+    "Remediation": {
+        "Code": {
+            "CLI": "",
+            "NativeIaC": "",
+            "Other": "",
+            "Terraform": ""
+          },
+        "Recommendation": {
+        "Text": "Review and modify security group rules for each VM instance.",
+        "Url": ""
+        }
+    },
+    "Categories": [],
+    "DependsOn": [],
+    "RelatedTo": [],
+    "Notes": "",
+    "RelatedUrl": "",
+    "Enabled": true,
+    "Compliances": []
+  }
+  
\ No newline at end of file
diff --git a/prowler/providers/nhn/services/compute/compute_instance_security_groups/compute_instance_security_groups.py b/prowler/providers/nhn/services/compute/compute_instance_security_groups/compute_instance_security_groups.py
new file mode 100644
index 0000000000..827683108f
--- /dev/null
+++ b/prowler/providers/nhn/services/compute/compute_instance_security_groups/compute_instance_security_groups.py
@@ -0,0 +1,27 @@
+from prowler.lib.check.models import Check, CheckReportNHN
+from prowler.providers.nhn.services.compute.compute_client import compute_client
+
+
+class compute_instance_security_groups(Check):
+    def execute(self):
+        findings = []
+        for instance in compute_client.instances:
+            report = CheckReportNHN(
+                metadata=self.metadata(),
+                resource=instance,
+                resource_name=instance.name,
+                resource_id=instance.id,
+                resource_location="kr1",
+            )
+            report.status = "PASS"
+            report.status_extended = (
+                f"VM Instance {instance.name} has a variety of security groups."
+            )
+            if instance.security_groups:
+                report.status = "FAIL"
+                report.status_extended = (
+                    f"VM Instance {instance.name} has only the default security group."
+                )
+            findings.append(report)
+
+        return findings
diff --git a/prowler/providers/nhn/services/compute/compute_service.py b/prowler/providers/nhn/services/compute/compute_service.py
new file mode 100644
index 0000000000..082c04bc96
--- /dev/null
+++ b/prowler/providers/nhn/services/compute/compute_service.py
@@ -0,0 +1,95 @@
+from pydantic import BaseModel
+
+from prowler.lib.logger import logger
+from prowler.providers.nhn.nhn_provider import NhnProvider
+
+
+class NHNComputeService:
+    def __init__(self, provider: NhnProvider):
+        self.session = provider.session
+        self.tenant_id = provider._tenant_id
+        self.endpoint = "https://kr1-api-instance.infrastructure.cloud.toast.com"
+
+        self.instances: list[Instance] = []
+        self._get_instances()
+
+    def _list_servers(self) -> list:
+        url = f"{self.endpoint}/v2/{self.tenant_id}/servers"
+        try:
+            response = self.session.get(url, timeout=10)
+            response.raise_for_status()
+            data = response.json()
+            return data.get("servers", [])
+        except Exception as e:
+            logger.error(f"Error listing servers: {e}")
+            return []
+
+    def _get_server_detail(self, server_id: str) -> dict:
+        url = f"{self.endpoint}/v2/{self.tenant_id}/servers/{server_id}"
+        try:
+            response = self.session.get(url, timeout=10)
+            response.raise_for_status()
+            return response.json()
+        except Exception as e:
+            logger.error(f"Error getting server detail {server_id}: {e}")
+            return {}
+
+    def _check_public_ip(self, server_info: dict) -> bool:
+        addresses = server_info.get("addresses", {})
+        for _, ip_list in addresses.items():
+            for ip_info in ip_list:
+                if ip_info.get("OS-EXT-IPS:type") == "floating":
+                    return True
+        return False
+
+    def _check_security_groups(self, server_info: dict) -> bool:
+        secruity_groups = server_info.get("security_groups", [])
+        sg_names = []
+        for sg_info in secruity_groups:
+            name = sg_info.get("name", "")
+            sg_names.append(name)
+
+        for name in sg_names:
+            if name != "default":
+                return False
+        return True
+
+    def _check_login_user(self, server_info: dict) -> bool:
+        metadata = server_info.get("metadata", {})
+        login_user = metadata.get("login_username", "")
+        if (
+            login_user == "Administrator"
+            or login_user == "root"
+            or login_user == "admin"
+        ):
+            return True
+        return False
+
+    def _get_instances(self):
+        server_list = self._list_servers()
+        for server in server_list:
+            server_id = server["id"]
+            server_name = server["name"]
+            detail = self._get_server_detail(server_id)
+            server_info = detail.get("server", {})
+
+            server_public_ip = self._check_public_ip(server_info)
+            server_security_groups = self._check_security_groups(server_info)
+            server_login_user = self._check_login_user(server_info)
+
+            instance = Instance(
+                id=server_id,
+                name=server_name,
+                public_ip=server_public_ip,
+                security_groups=server_security_groups,
+                login_user=server_login_user,
+            )
+            self.instances.append(instance)
+
+
+class Instance(BaseModel):
+    id: str
+    name: str
+    public_ip: bool
+    security_groups: bool
+    login_user: bool
diff --git a/prowler/providers/nhn/services/network/__init__.py b/prowler/providers/nhn/services/network/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/nhn/services/network/network_client.py b/prowler/providers/nhn/services/network/network_client.py
new file mode 100644
index 0000000000..9c53cfd3ba
--- /dev/null
+++ b/prowler/providers/nhn/services/network/network_client.py
@@ -0,0 +1,4 @@
+from prowler.providers.common.provider import Provider
+from prowler.providers.nhn.services.network.network_service import NHNNetworkService
+
+network_client = NHNNetworkService(Provider.get_global_provider())
diff --git a/prowler/providers/nhn/services/network/network_service.py b/prowler/providers/nhn/services/network/network_service.py
new file mode 100644
index 0000000000..140e1d2815
--- /dev/null
+++ b/prowler/providers/nhn/services/network/network_service.py
@@ -0,0 +1,89 @@
+from pydantic import BaseModel
+
+from prowler.lib.logger import logger
+from prowler.providers.nhn.nhn_provider import NhnProvider
+
+
+class Subnet(BaseModel):
+    name: str
+    external_router: bool
+    enable_dhcp: bool
+
+
+class Network(BaseModel):
+    id: str
+    name: str
+    empty_routingtables: bool
+    subnets: list[Subnet]
+
+
+class NHNNetworkService:
+    def __init__(self, provider: NhnProvider):
+        self.session = provider.session
+        self.tenant_id = provider._tenant_id
+        self.endpoint = "https://kr1-api-network-infrastructure.nhncloudservice.com"
+        self.networks: list[Network] = []
+        self._get_networks()
+
+    def _list_vpcs(self) -> list:
+        url = f"{self.endpoint}/v2.0/vpcs"
+        try:
+            response = self.session.get(url, timeout=10)
+            response.raise_for_status()
+            data = response.json()
+            return data.get("vpcs", [])
+        except Exception as e:
+            logger.error(f"Error listing vpcs: {e}")
+            return []
+
+    def _get_vpc_detail(self, vpc_id: str) -> dict:
+        url = f"{self.endpoint}/v2.0/vpcs/{vpc_id}"
+        try:
+            response = self.session.get(url, timeout=10)
+            response.raise_for_status()
+            return response.json()
+        except Exception as e:
+            logger.error(f"Error getting vpc detail {vpc_id}: {e}")
+            return {}
+
+    def _check_has_empty_routingtables(self, vpc_info: dict) -> bool:
+        routingtables = vpc_info.get("routingtables", [])
+        return not routingtables
+
+    def _check_subnet_has_external_router(self, subnet: dict) -> bool:
+        return subnet.get("router:external", True)
+
+    def _check_subnet_enable_dhcp(self, subnet: dict) -> bool:
+        return subnet.get("enable_dhcp", True)
+
+    def _get_networks(self):
+        vpc_list = self._list_vpcs()
+        for vpc in vpc_list:
+            vpc_id = vpc["id"]
+            vpc_name = vpc["name"]
+            detail = self._get_vpc_detail(vpc_id)
+            vpc_info = detail.get("vpc", {})
+            vpc_empty_routingtables = self._check_has_empty_routingtables(vpc_info)
+
+            network = Network(
+                id=vpc_id,
+                name=vpc_name,
+                empty_routingtables=vpc_empty_routingtables,
+                subnets=[],
+            )
+            self._get_subnets(vpc_info, network)
+            self.networks.append(network)
+
+    def _get_subnets(self, vpc_info: dict, network: Network):
+        subnet_list = vpc_info.get("subnets", [])
+        # ret_subnet_list = []
+        for subnet in subnet_list:
+            subnet_name = subnet["name"]
+            subnet_external_router = self._check_subnet_has_external_router(subnet)
+            subnet_enable_dhcp = self._check_subnet_enable_dhcp(subnet)
+            subnet_instance = Subnet(
+                name=subnet_name,
+                external_router=subnet_external_router,
+                enable_dhcp=subnet_enable_dhcp,
+            )
+            network.subnets.append(subnet_instance)
diff --git a/prowler/providers/nhn/services/network/network_vpc_has_empty_routingtables/__init__.py b/prowler/providers/nhn/services/network/network_vpc_has_empty_routingtables/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/nhn/services/network/network_vpc_has_empty_routingtables/network_vpc_has_empty_routingtables.metadata.json b/prowler/providers/nhn/services/network/network_vpc_has_empty_routingtables/network_vpc_has_empty_routingtables.metadata.json
new file mode 100644
index 0000000000..0865568f40
--- /dev/null
+++ b/prowler/providers/nhn/services/network/network_vpc_has_empty_routingtables/network_vpc_has_empty_routingtables.metadata.json
@@ -0,0 +1,33 @@
+{
+    "Provider": "nhn",
+    "CheckID": "network_vpc_has_empty_routingtables",
+    "CheckTitle": "Check if VPC has empty routing tables",
+    "CheckType": [], 
+    "ServiceName": "network",
+    "SubServiceName": "",
+    "ResourceIdTemplate": "",
+    "Risk": "",
+    "Severity": "medium",
+    "ResourceType": "VPC",
+    "Description": "This check verifies if the VPC has empty routing tables. Having empty routing tables may indicate misconfiguration or incomplete network setup.",
+    "Remediation": {
+        "Code": {
+            "CLI": "",
+            "NativeIaC": "",
+            "Other": "",
+            "Terraform": ""
+          },
+        "Recommendation": {
+        "Text": "Ensure that the VPC has properly configured routing tables with necessary routes to ensure proper network connectivity. If not needed, delete the empty routing tables.",
+        "Url": ""
+        }
+    },
+    "Categories": [],
+    "DependsOn": [],
+    "RelatedTo": [],
+    "Notes": "",
+    "RelatedUrl": "",
+    "Enabled": true,
+    "Compliances": []
+  }
+  
\ No newline at end of file
diff --git a/prowler/providers/nhn/services/network/network_vpc_has_empty_routingtables/network_vpc_has_empty_routingtables.py b/prowler/providers/nhn/services/network/network_vpc_has_empty_routingtables/network_vpc_has_empty_routingtables.py
new file mode 100644
index 0000000000..946b2ae3a0
--- /dev/null
+++ b/prowler/providers/nhn/services/network/network_vpc_has_empty_routingtables/network_vpc_has_empty_routingtables.py
@@ -0,0 +1,25 @@
+from prowler.lib.check.models import Check, CheckReportNHN
+from prowler.providers.nhn.services.network.network_client import network_client
+
+
+class network_vpc_has_empty_routingtables(Check):
+    def execute(self):
+        findings = []
+        for network in network_client.networks:
+            report = CheckReportNHN(
+                metadata=self.metadata(),
+                resource=network,
+                resource_name=network.name,
+                resource_id=network.id,
+                resource_location="kr1",
+            )
+            report.status = "PASS"
+            report.status_extended = (
+                f"VPC {network.name} does not have empty routingtables."
+            )
+            if network.empty_routingtables:
+                report.status = "FAIL"
+                report.status_extended = f"VPC {network.name} has empty routingtables."
+            findings.append(report)
+
+        return findings
diff --git a/prowler/providers/nhn/services/network/network_vpc_subnet_enable_dhcp/__init__.py b/prowler/providers/nhn/services/network/network_vpc_subnet_enable_dhcp/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/nhn/services/network/network_vpc_subnet_enable_dhcp/network_vpc_subnet_enable_dhcp.metadata.json b/prowler/providers/nhn/services/network/network_vpc_subnet_enable_dhcp/network_vpc_subnet_enable_dhcp.metadata.json
new file mode 100644
index 0000000000..52ff213411
--- /dev/null
+++ b/prowler/providers/nhn/services/network/network_vpc_subnet_enable_dhcp/network_vpc_subnet_enable_dhcp.metadata.json
@@ -0,0 +1,33 @@
+{
+    "Provider": "nhn",
+    "CheckID": "network_vpc_subnet_enable_dhcp",
+    "CheckTitle": "Check if DHCP is enabled for subnets in VPC",
+    "CheckType": [], 
+    "ServiceName": "network",
+    "SubServiceName": "",
+    "ResourceIdTemplate": "",
+    "Risk": "",
+    "Severity": "Medium",
+    "ResourceType": "VPC",
+    "Description": "This check verifies if DHCP is enabled for the subnets in the VPC.",
+    "Remediation": {
+        "Code": {
+            "CLI": "",
+            "NativeIaC": "",
+            "Other": "",
+            "Terraform": ""
+          },
+        "Recommendation": {
+        "Text": "Ensure that DHCP is enabled for all subnets where automatic IP address allocation is needed.",
+        "Url": ""
+        }
+    },
+    "Categories": [],
+    "DependsOn": [],
+    "RelatedTo": [],
+    "Notes": "",
+    "RelatedUrl": "",
+    "Enabled": true,
+    "Compliances": []
+  }
+  
\ No newline at end of file
diff --git a/prowler/providers/nhn/services/network/network_vpc_subnet_enable_dhcp/network_vpc_subnet_enable_dhcp.py b/prowler/providers/nhn/services/network/network_vpc_subnet_enable_dhcp/network_vpc_subnet_enable_dhcp.py
new file mode 100644
index 0000000000..cb0f77b087
--- /dev/null
+++ b/prowler/providers/nhn/services/network/network_vpc_subnet_enable_dhcp/network_vpc_subnet_enable_dhcp.py
@@ -0,0 +1,26 @@
+from prowler.lib.check.models import Check, CheckReportNHN
+from prowler.providers.nhn.services.network.network_client import network_client
+
+
+class network_vpc_subnet_enable_dhcp(Check):
+    def execute(self):
+        findings = []
+        for network in network_client.networks:
+            for subnet in network.subnets:
+                report = CheckReportNHN(
+                    metadata=self.metadata(),
+                    resource=network,
+                    resource_name=network.name,
+                    resource_id=network.id,
+                    resource_location="kr1",
+                )
+                report.status = "PASS"
+                report.status_extended = f"VPC {network.name} Subnet {subnet.name} does not have DHCP enabled."
+                if subnet.enable_dhcp:
+                    report.status = "FAIL"
+                    report.status_extended = (
+                        f"VPC {network.name} Subnet {subnet.name} has DHCP enabled."
+                    )
+                findings.append(report)
+
+            return findings
diff --git a/prowler/providers/nhn/services/network/network_vpc_subnet_has_external_router/__init__.py b/prowler/providers/nhn/services/network/network_vpc_subnet_has_external_router/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/nhn/services/network/network_vpc_subnet_has_external_router/network_vpc_subnet_has_external_router.metadata.json b/prowler/providers/nhn/services/network/network_vpc_subnet_has_external_router/network_vpc_subnet_has_external_router.metadata.json
new file mode 100644
index 0000000000..e36a097a52
--- /dev/null
+++ b/prowler/providers/nhn/services/network/network_vpc_subnet_has_external_router/network_vpc_subnet_has_external_router.metadata.json
@@ -0,0 +1,33 @@
+{
+    "Provider": "nhn",
+    "CheckID": "network_vpc_subnet_has_external_router",
+    "CheckTitle": "Check for External Router in NHN VPC Subnet",
+    "CheckType": [], 
+    "ServiceName": "network",
+    "SubServiceName": "",
+    "ResourceIdTemplate": "",
+    "Risk": "",
+    "Severity": "medium",
+    "ResourceType": "VPC",
+    "Description": "Checks if a VPC allows access from the public internet, by verifying if an external router is configured.",
+    "Remediation": {
+        "Code": {
+            "CLI": "",
+            "NativeIaC": "",
+            "Other": "",
+            "Terraform": ""
+          },
+        "Recommendation": {
+        "Text": "Review the external router settings for the VPC Subnet.",
+        "Url": ""
+        }
+    },
+    "Categories": [],
+    "DependsOn": [],
+    "RelatedTo": [],
+    "Notes": "",
+    "RelatedUrl": "",
+    "Enabled": true,
+    "Compliances": []
+  }
+  
\ No newline at end of file
diff --git a/prowler/providers/nhn/services/network/network_vpc_subnet_has_external_router/network_vpc_subnet_has_external_router.py b/prowler/providers/nhn/services/network/network_vpc_subnet_has_external_router/network_vpc_subnet_has_external_router.py
new file mode 100644
index 0000000000..52b49137f3
--- /dev/null
+++ b/prowler/providers/nhn/services/network/network_vpc_subnet_has_external_router/network_vpc_subnet_has_external_router.py
@@ -0,0 +1,24 @@
+from prowler.lib.check.models import Check, CheckReportNHN
+from prowler.providers.nhn.services.network.network_client import network_client
+
+
+class network_vpc_subnet_has_external_router(Check):
+    def execute(self):
+        findings = []
+        for network in network_client.networks:
+            for subnet in network.subnets:
+                report = CheckReportNHN(
+                    metadata=self.metadata(),
+                    resource=network,
+                    resource_name=network.name,
+                    resource_id=network.id,
+                    resource_location="kr1",
+                )
+                report.status = "PASS"
+                report.status_extended = f"VPC {network.name} Subnet {subnet.name} does not have an external router."
+                if subnet.external_router:
+                    report.status = "FAIL"
+                    report.status_extended = f"VPC {network.name} Subnet {subnet.name} has an external router."
+                findings.append(report)
+
+            return findings
diff --git a/tests/lib/cli/parser_test.py b/tests/lib/cli/parser_test.py
index 16b322ac9c..37bec15d94 100644
--- a/tests/lib/cli/parser_test.py
+++ b/tests/lib/cli/parser_test.py
@@ -16,11 +16,11 @@
 
 # capsys
 # https://docs.pytest.org/en/7.1.x/how-to/capture-stdout-stderr.html
-prowler_default_usage_error = "usage: prowler [-h] [--version] {aws,azure,gcp,kubernetes,microsoft365,dashboard} ..."
+prowler_default_usage_error = "usage: prowler [-h] [--version] {aws,azure,gcp,kubernetes,microsoft365,nhn,dashboard} ..."
 
 
 def mock_get_available_providers():
-    return ["aws", "azure", "gcp", "kubernetes", "microsoft365"]
+    return ["aws", "azure", "gcp", "kubernetes", "microsoft365", "nhn"]
 
 
 @pytest.mark.arg_parser
diff --git a/tests/providers/nhn/lib/mutelist/fixtures/nhn_mutelist.yaml b/tests/providers/nhn/lib/mutelist/fixtures/nhn_mutelist.yaml
new file mode 100644
index 0000000000..6a4be42b4b
--- /dev/null
+++ b/tests/providers/nhn/lib/mutelist/fixtures/nhn_mutelist.yaml
@@ -0,0 +1,16 @@
+### Account, Check and/or Region can be * to apply for all the cases.
+### Resources and tags are lists that can have either Regex or Keywords.
+### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together.
+### Use an alternation Regex to match one of multiple tags with "ORed" logic.
+### For each check you can except Accounts, Regions, Resources and/or Tags.
+###########################  MUTELIST EXAMPLE  ###########################
+Mutelist:
+  Accounts:
+    "subscription_1":
+      Checks:
+        "compute_instance_public_ip":
+          Regions:
+            - "*"
+          Resources:
+            - "resource_1"
+            - "resource_2"
\ No newline at end of file
diff --git a/tests/providers/nhn/lib/mutelist/nhn_mutelist_test.py b/tests/providers/nhn/lib/mutelist/nhn_mutelist_test.py
new file mode 100644
index 0000000000..dcdc17b2d9
--- /dev/null
+++ b/tests/providers/nhn/lib/mutelist/nhn_mutelist_test.py
@@ -0,0 +1,100 @@
+import yaml
+from mock import MagicMock
+
+from prowler.providers.nhn.lib.mutelist.mutelist import NHNMutelist
+from tests.lib.outputs.fixtures.fixtures import generate_finding_output
+
+MUTELIST_FIXTURE_PATH = "tests/providers/nhn/lib/mutelist/fixtures/nhn_mutelist.yaml"
+
+
+class TestNHNMutelist:
+    def test_get_mutelist_file_from_local_file(self):
+        mutelist = NHNMutelist(mutelist_path=MUTELIST_FIXTURE_PATH)
+
+        with open(MUTELIST_FIXTURE_PATH) as f:
+            mutelist_fixture = yaml.safe_load(f)["Mutelist"]
+
+        assert mutelist.mutelist == mutelist_fixture
+        assert mutelist.mutelist_file_path == MUTELIST_FIXTURE_PATH
+
+    def test_get_mutelist_file_from_local_file_non_existent(self):
+        mutelist_path = "tests/lib/mutelist/fixtures/not_present"
+        mutelist = NHNMutelist(mutelist_path=mutelist_path)
+
+        assert mutelist.mutelist == {}
+        assert mutelist.mutelist_file_path == mutelist_path
+
+    def test_validate_mutelist_not_valid_key(self):
+        mutelist_path = MUTELIST_FIXTURE_PATH
+        with open(mutelist_path) as f:
+            mutelist_fixture = yaml.safe_load(f)["Mutelist"]
+
+        mutelist_fixture["Accounts1"] = mutelist_fixture["Accounts"]
+        del mutelist_fixture["Accounts"]
+
+        mutelist = NHNMutelist(mutelist_content=mutelist_fixture)
+
+        assert not mutelist.validate_mutelist()
+        assert mutelist.mutelist == {}
+        assert mutelist.mutelist_file_path is None
+
+    def test_is_finding_muted(self):
+        # Mutelist
+        mutelist_content = {
+            "Accounts": {
+                "resource_1": {
+                    "Checks": {
+                        "check_test": {
+                            "Regions": ["*"],
+                            "Resources": ["test_resource"],
+                        }
+                    }
+                }
+            }
+        }
+
+        mutelist = NHNMutelist(mutelist_content=mutelist_content)
+
+        finding = MagicMock()
+        finding.resource_id = "resource_1"
+        finding.check_metadata = MagicMock()
+        finding.check_metadata.CheckID = "check_test"
+        finding.status = "FAIL"
+        finding.resource_name = "test_resource"
+        finding.location = "test_region"
+        finding.resource_tags = []
+
+        assert mutelist.is_finding_muted(finding)
+
+    def test_mute_finding(self):
+        # Mutelist
+        mutelist_content = {
+            "Accounts": {
+                "resource_1": {
+                    "Checks": {
+                        "check_test": {
+                            "Regions": ["*"],
+                            "Resources": ["test_resource"],
+                        }
+                    }
+                }
+            }
+        }
+
+        mutelist = NHNMutelist(mutelist_content=mutelist_content)
+
+        finding_1 = generate_finding_output(
+            check_id="check_test",
+            status="FAIL",
+            account_uid="resource_1",
+            region="test_region",
+            resource_uid="test_resource",
+            resource_tags=[],
+            muted=False,
+        )
+
+        muted_finding = mutelist.mute_finding(finding=finding_1)
+
+        assert muted_finding.status == "MUTED"
+        assert muted_finding.muted
+        assert muted_finding.raw["status"] == "FAIL"
diff --git a/tests/providers/nhn/nhn_fixtures.py b/tests/providers/nhn/nhn_fixtures.py
new file mode 100644
index 0000000000..a8a1fbd53e
--- /dev/null
+++ b/tests/providers/nhn/nhn_fixtures.py
@@ -0,0 +1,29 @@
+from mock import MagicMock
+
+from prowler.providers.nhn.nhn_provider import NhnProvider
+
+
+def set_mocked_nhn_provider(
+    username="test_user",
+    password="test_password",
+    tenant_id="tenant123",
+    audit_config=None,
+    fixer_config=None,
+):
+    """
+    Creates a mocked NHN Provider object for testing without real network calls.
+    """
+    provider = MagicMock(spec=NhnProvider)  # or just MagicMock()
+
+    provider.type = "nhn"
+    provider._username = username
+    provider._password = password
+    provider._tenant_id = tenant_id
+    provider._token = "fake_keystone_token"
+
+    provider.session = MagicMock()
+
+    provider.audit_config = audit_config
+    provider.fixer_config = fixer_config
+
+    return provider
diff --git a/tests/providers/nhn/nhn_provider_test.py b/tests/providers/nhn/nhn_provider_test.py
new file mode 100644
index 0000000000..dc59ac1347
--- /dev/null
+++ b/tests/providers/nhn/nhn_provider_test.py
@@ -0,0 +1,157 @@
+import os
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from prowler.providers.common.models import Connection
+from prowler.providers.nhn.nhn_provider import NhnProvider
+
+
+class TestNhnProvider:
+    @patch.dict(
+        os.environ,
+        {
+            "NHN_USERNAME": "env_user",
+            "NHN_PASSWORD": "env_pass",
+            "NHN_TENANT_ID": "env_tenant",
+        },
+    )
+    @patch("prowler.providers.nhn.nhn_provider.load_and_validate_config_file")
+    @patch("requests.post")
+    def test_nhn_provider_init_success(self, mock_post, mock_load_config):
+        """
+        Test a successful initialization of NhnProvider
+        with valid username/password/tenant_id and a Keystone token response = 200.
+        """
+        # 1) Mock load_and_validate_config_file to avoid reading real config file
+        mock_load_config.return_value = {}
+
+        # 2) Mock the requests.post to simulate a successful token response
+        mock_response = MagicMock()
+        mock_response.status_code = 200
+        mock_response.json.return_value = {
+            "access": {"token": {"id": "fake_keystone_token"}}
+        }
+        mock_post.return_value = mock_response
+
+        # 3) Create provider
+        provider = NhnProvider(
+            username="test_user",
+            password="test_pass",
+            tenant_id="test_tenant",
+        )
+
+        # 4) Assertions
+        assert provider._token == "fake_keystone_token"
+        assert provider.session is not None
+        assert provider.session.headers["X-Auth-Token"] == "fake_keystone_token"
+
+    @patch.dict(os.environ, {}, clear=True)
+    @patch(
+        "prowler.providers.nhn.nhn_provider.load_and_validate_config_file",
+        return_value={},
+    )
+    def test_nhn_provider_init_missing_args(self, mock_load_config):
+        """
+        Test initialization when username/password/tenant_id is missing => ValueError
+        """
+        with pytest.raises(ValueError) as exc_info:
+            NhnProvider(username="", password="secret", tenant_id="tenant")
+        assert "requires username, password and tenant_id" in str(exc_info.value)
+
+    @patch(
+        "prowler.providers.nhn.nhn_provider.load_and_validate_config_file",
+        return_value={},
+    )
+    @patch("requests.post")
+    def test_nhn_provider_init_token_fail(self, mock_post, mock_load_config):
+        """
+        Test the case where Keystone token request fails (non-200)
+        => provider._session remains None
+        """
+        mock_post.return_value.status_code = 401
+        mock_post.return_value.text = "Unauthorized"
+
+        with pytest.raises(ValueError) as exc_info:
+            NhnProvider(
+                username="test_user",
+                password="test_pass",
+                tenant_id="tenant123",
+            )
+
+        assert "Failed to get NHN token" in str(exc_info.value)
+
+    @patch("prowler.providers.nhn.nhn_provider.requests")
+    def test_test_connection_success(self, mock_requests):
+        """
+        Test test_connection static method => success case
+        """
+        # 1) Mock token success
+        mock_post_response = MagicMock()
+        mock_post_response.status_code = 200
+        mock_post_response.json.return_value = {
+            "access": {"token": {"id": "fake_keystone_token"}}
+        }
+        # 2) Mock /servers success
+        mock_get_response = MagicMock()
+        mock_get_response.status_code = 200
+        mock_requests.post.return_value = mock_post_response
+        mock_requests.get.return_value = mock_get_response
+
+        conn = NhnProvider.test_connection(
+            username="test_user",
+            password="test_pass",
+            tenant_id="tenant123",
+            raise_on_exception=True,
+        )
+
+        assert isinstance(conn, Connection)
+        assert conn.is_connected is True
+        assert conn.error is None
+
+    @patch("prowler.providers.nhn.nhn_provider.requests")
+    def test_test_connection_token_fail(self, mock_requests):
+        """
+        Test test_connection => token request fails => returns Connection(error=...)
+        """
+        mock_requests.post.return_value.status_code = 403
+        mock_requests.post.return_value.text = "Forbidden"
+
+        conn = NhnProvider.test_connection(
+            username="bad_user",
+            password="bad_pass",
+            tenant_id="tenant123",
+            raise_on_exception=False,  # so we don't raise, we get Connection object
+        )
+        assert conn.is_connected is False
+        assert conn.error is not None
+        assert "Failed to get token" in str(conn.error)
+
+    @patch("prowler.providers.nhn.nhn_provider.requests")
+    def test_test_connection_servers_fail(self, mock_requests):
+        """
+        Test test_connection => token OK, but /servers fails => returns Connection(error=...)
+        """
+        # Keystone token success
+        mock_post_response = MagicMock()
+        mock_post_response.status_code = 200
+        mock_post_response.json.return_value = {
+            "access": {"token": {"id": "fake_keystone_token"}}
+        }
+        mock_requests.post.return_value = mock_post_response
+
+        # /servers fail
+        mock_get_response = MagicMock()
+        mock_get_response.status_code = 500
+        mock_get_response.text = "Internal Server Error"
+        mock_requests.get.return_value = mock_get_response
+
+        conn = NhnProvider.test_connection(
+            username="test_user",
+            password="test_pass",
+            tenant_id="tenant123",
+            raise_on_exception=False,
+        )
+        assert conn.is_connected is False
+        assert conn.error is not None
+        assert "/servers call failed" in str(conn.error)
diff --git a/tests/providers/nhn/services/compute/compute_instance_login_user/compute_instance_login_user_test_for_nhn.py b/tests/providers/nhn/services/compute/compute_instance_login_user/compute_instance_login_user_test_for_nhn.py
new file mode 100644
index 0000000000..5e545d87c3
--- /dev/null
+++ b/tests/providers/nhn/services/compute/compute_instance_login_user/compute_instance_login_user_test_for_nhn.py
@@ -0,0 +1,110 @@
+from unittest import mock
+from uuid import uuid4
+
+from prowler.providers.nhn.services.compute.compute_service import Instance
+from tests.providers.nhn.nhn_fixtures import set_mocked_nhn_provider
+
+
+class Test_compute_instance_login_user:
+    def test_no_instances(self):
+        # 1) Make a MagicMock for compute_client
+        compute_client = mock.MagicMock()
+        compute_client.instances = []
+
+        # 2) Patch get_global_provider() to return a mocked NHN provider
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                # patch the 'compute_instance_login_user.compute_client' used in the check code
+                "prowler.providers.nhn.services.compute.compute_instance_login_user.compute_instance_login_user.compute_client",
+                new=compute_client,
+            ),
+        ):
+            # 3) Import the check code AFTER patching
+            from prowler.providers.nhn.services.compute.compute_instance_login_user.compute_instance_login_user import (
+                compute_instance_login_user,
+            )
+
+            # 4) Run the check
+            check = compute_instance_login_user()
+            result = check.execute()
+
+            # 5) Assertions
+            assert len(result) == 0  # no instances => no findings
+
+    def test_has_instance_non_admin_login(self):
+        # Make a MagicMock for compute_client
+        compute_client = mock.MagicMock()
+
+        # Suppose we have 1 instance with login_user=False => PASS expected
+        instance_id = str(uuid4())
+        instance_name = "testVM"
+        mock_instance = mock.MagicMock(spec=Instance)
+        mock_instance.id = instance_id
+        mock_instance.name = instance_name
+        mock_instance.login_user = False  # => means not admin login
+        compute_client.instances = [mock_instance]
+
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                "prowler.providers.nhn.services.compute.compute_instance_login_user.compute_instance_login_user.compute_client",
+                new=compute_client,
+            ),
+        ):
+            from prowler.providers.nhn.services.compute.compute_instance_login_user.compute_instance_login_user import (
+                compute_instance_login_user,
+            )
+
+            check = compute_instance_login_user()
+            result = check.execute()
+
+            assert len(result) == 1
+            assert result[0].status == "PASS"
+            assert "has a appropriate login user" in result[0].status_extended
+            assert result[0].resource_name == instance_name
+            assert result[0].resource_id == instance_id
+
+    def test_has_instance_admin_login(self):
+        # Another scenario: instance with login_user=True => FAIL expected
+        compute_client = mock.MagicMock()
+
+        instance_id = str(uuid4())
+        instance_name = "rootVM"
+        mock_instance = mock.MagicMock(spec=Instance)
+        mock_instance.id = instance_id
+        mock_instance.name = instance_name
+        mock_instance.login_user = True  # => admin or root user
+        compute_client.instances = [mock_instance]
+
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                "prowler.providers.nhn.services.compute.compute_instance_login_user.compute_instance_login_user.compute_client",
+                new=compute_client,
+            ),
+        ):
+            from prowler.providers.nhn.services.compute.compute_instance_login_user.compute_instance_login_user import (
+                compute_instance_login_user,
+            )
+
+            check = compute_instance_login_user()
+            result = check.execute()
+
+            assert len(result) == 1
+            assert result[0].status == "FAIL"
+            assert (
+                "has an Administrative(admin/root) login user"
+                in result[0].status_extended
+            )
+            assert result[0].resource_name == instance_name
+            assert result[0].resource_id == instance_id
diff --git a/tests/providers/nhn/services/compute/compute_instance_public_ip/compute_instance_public_ip_test_for_nhn.py b/tests/providers/nhn/services/compute/compute_instance_public_ip/compute_instance_public_ip_test_for_nhn.py
new file mode 100644
index 0000000000..2b72460c78
--- /dev/null
+++ b/tests/providers/nhn/services/compute/compute_instance_public_ip/compute_instance_public_ip_test_for_nhn.py
@@ -0,0 +1,107 @@
+from unittest import mock
+from uuid import uuid4
+
+from prowler.providers.nhn.services.compute.compute_service import Instance
+from tests.providers.nhn.nhn_fixtures import set_mocked_nhn_provider
+
+
+class Test_compute_instance_public_ip:
+    def test_no_instances(self):
+        # 1) Make a MagicMock for compute_client
+        compute_client = mock.MagicMock()
+        compute_client.instances = []
+
+        # 2) Patch get_global_provider() to return a mocked NHN provider
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                # patch the 'compute_instance_public_ip.compute_client' used in the check code
+                "prowler.providers.nhn.services.compute.compute_instance_public_ip.compute_instance_public_ip.compute_client",
+                new=compute_client,
+            ),
+        ):
+            # 3) Import the check code AFTER patching
+            from prowler.providers.nhn.services.compute.compute_instance_public_ip.compute_instance_public_ip import (
+                compute_instance_public_ip,
+            )
+
+            # 4) Run the check
+            check = compute_instance_public_ip()
+            result = check.execute()
+
+            # 5) Assertions
+            assert len(result) == 0  # no instances => no findings
+
+    def test_has_instance_non_public_ip(self):
+        # Make a MagicMock for compute_client
+        compute_client = mock.MagicMock()
+
+        # Suppose we have 1 instance with public_ip=False => PASS expected
+        instance_id = str(uuid4())
+        instance_name = "testVM"
+        mock_instance = mock.MagicMock(spec=Instance)
+        mock_instance.id = instance_id
+        mock_instance.name = instance_name
+        mock_instance.public_ip = False  # => means does not have public IP
+        compute_client.instances = [mock_instance]
+
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                "prowler.providers.nhn.services.compute.compute_instance_public_ip.compute_instance_public_ip.compute_client",
+                new=compute_client,
+            ),
+        ):
+            from prowler.providers.nhn.services.compute.compute_instance_public_ip.compute_instance_public_ip import (
+                compute_instance_public_ip,
+            )
+
+            check = compute_instance_public_ip()
+            result = check.execute()
+
+            assert len(result) == 1
+            assert result[0].status == "PASS"
+            assert "does not have a public IP" in result[0].status_extended
+            assert result[0].resource_name == instance_name
+            assert result[0].resource_id == instance_id
+
+    def test_has_instance_public_ip(self):
+        # Another scenario: instance with public_ip=True => FAIL expected
+        compute_client = mock.MagicMock()
+
+        instance_id = str(uuid4())
+        instance_name = "rootVM"
+        mock_instance = mock.MagicMock(spec=Instance)
+        mock_instance.id = instance_id
+        mock_instance.name = instance_name
+        mock_instance.public_ip = True  # => means has public IP
+        compute_client.instances = [mock_instance]
+
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                "prowler.providers.nhn.services.compute.compute_instance_public_ip.compute_instance_public_ip.compute_client",
+                new=compute_client,
+            ),
+        ):
+            from prowler.providers.nhn.services.compute.compute_instance_public_ip.compute_instance_public_ip import (
+                compute_instance_public_ip,
+            )
+
+            check = compute_instance_public_ip()
+            result = check.execute()
+
+            assert len(result) == 1
+            assert result[0].status == "FAIL"
+            assert "has a public IP" in result[0].status_extended
+            assert result[0].resource_name == instance_name
+            assert result[0].resource_id == instance_id
diff --git a/tests/providers/nhn/services/compute/compute_instance_security__groups/compute_instance_security_groups_test_for_nhn.py b/tests/providers/nhn/services/compute/compute_instance_security__groups/compute_instance_security_groups_test_for_nhn.py
new file mode 100644
index 0000000000..194c80be2c
--- /dev/null
+++ b/tests/providers/nhn/services/compute/compute_instance_security__groups/compute_instance_security_groups_test_for_nhn.py
@@ -0,0 +1,108 @@
+from unittest import mock
+from uuid import uuid4
+
+from prowler.providers.nhn.services.compute.compute_service import Instance
+from tests.providers.nhn.nhn_fixtures import set_mocked_nhn_provider
+
+
+class Test_compute_instance_security_groups:
+    def test_no_instances(self):
+        # 1) Make a MagicMock for compute_client
+        compute_client = mock.MagicMock()
+        compute_client.instances = []
+
+        # 2) Patch get_global_provider() to return a mocked NHN provider
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                # patch the 'compute_instance_security_groups.compute_client' used in the check code
+                "prowler.providers.nhn.services.compute.compute_instance_security_groups.compute_instance_security_groups.compute_client",
+                new=compute_client,
+            ),
+        ):
+            # 3) Import the check code AFTER patching
+            from prowler.providers.nhn.services.compute.compute_instance_security_groups.compute_instance_security_groups import (
+                compute_instance_security_groups,
+            )
+
+            # 4) Run the check
+            check = compute_instance_security_groups()
+            result = check.execute()
+
+            # 5) Assertions
+            assert len(result) == 0  # no instances => no findings
+
+    def test_has_instance_variety_security_groups(self):
+        # Make a MagicMock for compute_client
+        compute_client = mock.MagicMock()
+
+        # Suppose we have 1 instance with security_groups=False => PASS expected
+        instance_id = str(uuid4())
+        instance_name = "testVM"
+        mock_instance = mock.MagicMock(spec=Instance)
+        mock_instance.id = instance_id
+        mock_instance.name = instance_name
+        mock_instance.security_groups = False  # => means has variety of security groups
+        compute_client.instances = [mock_instance]
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                "prowler.providers.nhn.services.compute.compute_instance_security_groups.compute_instance_security_groups.compute_client",
+                new=compute_client,
+            ),
+        ):
+            from prowler.providers.nhn.services.compute.compute_instance_security_groups.compute_instance_security_groups import (
+                compute_instance_security_groups,
+            )
+
+            check = compute_instance_security_groups()
+            result = check.execute()
+
+            assert len(result) == 1
+            assert result[0].status == "PASS"
+            assert "has a variety of security groups" in result[0].status_extended
+            assert result[0].resource_name == instance_name
+            assert result[0].resource_id == instance_id
+
+    def test_has_instance_security_groups(self):
+        # Another scenario: instance with security_groups=True => FAIL expected
+        compute_client = mock.MagicMock()
+
+        instance_id = str(uuid4())
+        instance_name = "rootVM"
+        mock_instance = mock.MagicMock(spec=Instance)
+        mock_instance.id = instance_id
+        mock_instance.name = instance_name
+        mock_instance.security_groups = (
+            True  # => means has only the default security group
+        )
+        compute_client.instances = [mock_instance]
+
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                "prowler.providers.nhn.services.compute.compute_instance_security_groups.compute_instance_security_groups.compute_client",
+                new=compute_client,
+            ),
+        ):
+            from prowler.providers.nhn.services.compute.compute_instance_security_groups.compute_instance_security_groups import (
+                compute_instance_security_groups,
+            )
+
+            check = compute_instance_security_groups()
+            result = check.execute()
+
+            assert len(result) == 1
+            assert result[0].status == "FAIL"
+            assert "has only the default security group" in result[0].status_extended
+            assert result[0].resource_name == instance_name
+            assert result[0].resource_id == instance_id
diff --git a/tests/providers/nhn/services/compute/compute_service_test_for_nhn.py b/tests/providers/nhn/services/compute/compute_service_test_for_nhn.py
new file mode 100644
index 0000000000..d5a8726b8d
--- /dev/null
+++ b/tests/providers/nhn/services/compute/compute_service_test_for_nhn.py
@@ -0,0 +1,100 @@
+from unittest.mock import MagicMock, patch
+
+from prowler.providers.nhn.services.compute.compute_service import NHNComputeService
+from tests.providers.nhn.nhn_fixtures import set_mocked_nhn_provider
+
+
+class TestNHNComputeService:
+    @patch("prowler.providers.nhn.services.compute.compute_service.logger")
+    def test_compute_service_basic(self, mock_logger):
+        """
+        Test that NHNComputeService properly calls _list_servers(),
+        _get_server_detail() for each server, and populates self.instances.
+        """
+        # create a mocked NHN Provider
+        provider = set_mocked_nhn_provider(
+            username="testUser",
+            password="testPass",
+            tenant_id="tenant123",
+        )
+
+        # define mocked responses
+        mocked_response_servers = MagicMock()
+        mocked_response_servers.status_code = 200
+        mocked_response_servers.json.return_value = {
+            "servers": [
+                {"id": "server1", "name": "myserver1"},
+                {"id": "server2", "name": "myserver2"},
+            ]
+        }
+
+        mocked_response_server1 = MagicMock()
+        mocked_response_server1.status_code = 200
+        mocked_response_server1.json.return_value = {
+            "server": {
+                "addresses": {
+                    "vpc1": [
+                        {"OS-EXT-IPS:type": "floating"},
+                    ]
+                },
+                "security_groups": [{"name": "default"}],
+                "metadata": {"login_username": "root"},
+            }
+        }
+
+        mocked_response_server2 = MagicMock()
+        mocked_response_server2.status_code = 200
+        mocked_response_server2.json.return_value = {
+            "server": {
+                "addresses": {
+                    "vpc1": [
+                        {"OS-EXT-IPS:type": "fixed"},
+                    ]
+                },
+                "security_groups": [{"name": "default"}, {"name": "other-sg"}],
+                "metadata": {"login_username": "regularuser"},
+            }
+        }
+
+        def get_side_effect(url, timeout=10):
+            print(f"Called with timeout={timeout}")
+            if (
+                "/v2/tenant123/servers" in url
+                and not url.endswith("server1")
+                and not url.endswith("server2")
+            ):
+                return mocked_response_servers
+            elif url.endswith("server1"):
+                return mocked_response_server1
+            elif url.endswith("server2"):
+                return mocked_response_server2
+            else:
+                mock_404 = MagicMock()
+                mock_404.status_code = 404
+                mock_404.text = "Not Found"
+                return mock_404
+
+        provider.session.get.side_effect = get_side_effect
+
+        # create NHNComputeService, which internally calls _get_instances()
+        compute_service = NHNComputeService(provider)
+
+        assert len(compute_service.instances) == 2
+
+        # first instance
+        inst1 = compute_service.instances[0]
+        assert inst1.id == "server1"
+        assert inst1.name == "myserver1"
+        assert inst1.public_ip is True
+        assert inst1.security_groups is True
+        assert inst1.login_user is True
+
+        # second instance
+        inst2 = compute_service.instances[1]
+        assert inst2.id == "server2"
+        assert inst2.name == "myserver2"
+        assert inst2.public_ip is False
+        assert inst2.security_groups is False
+        assert inst2.login_user is False
+
+        mock_logger.error.assert_not_called()
diff --git a/tests/providers/nhn/services/network/network_service_test_for_nhn.py b/tests/providers/nhn/services/network/network_service_test_for_nhn.py
new file mode 100644
index 0000000000..ca781e8e75
--- /dev/null
+++ b/tests/providers/nhn/services/network/network_service_test_for_nhn.py
@@ -0,0 +1,98 @@
+from unittest.mock import MagicMock, patch
+
+from prowler.providers.nhn.services.network.network_service import NHNNetworkService
+from tests.providers.nhn.nhn_fixtures import set_mocked_nhn_provider
+
+
+class TestNHNNetworkService:
+    @patch("prowler.providers.nhn.services.network.network_service.logger")
+    def test_network_service_basic(self, mock_logger):
+        """
+        Test that NHNNetworkService correctly calls _list_vpcs(),
+        _get_vpc_detail() for each VPC, and populates self.networks and self.subnets.
+        """
+        # create a mocked NHN Provider
+        provider = set_mocked_nhn_provider(
+            username="testUser",
+            password="testPass",
+            tenant_id="tenant123",
+        )
+
+        # define mocked responses for VPCs and Subnets
+        mocked_response_vpcs = MagicMock()
+        mocked_response_vpcs.status_code = 200
+        mocked_response_vpcs.json.return_value = {
+            "vpcs": [
+                {"id": "vpc1", "name": "myvpc1"},
+                {"id": "vpc2", "name": "myvpc2"},
+            ]
+        }
+
+        mocked_response_vpc1 = MagicMock()
+        mocked_response_vpc1.status_code = 200
+        mocked_response_vpc1.json.return_value = {
+            "vpc": {
+                "routingtables": [],
+                "subnets": [
+                    {"name": "subnet1", "router:external": True, "enable_dhcp": False},
+                ],
+            }
+        }
+
+        mocked_response_vpc2 = MagicMock()
+        mocked_response_vpc2.status_code = 200
+        mocked_response_vpc2.json.return_value = {
+            "vpc": {
+                "routingtables": [{"id": "rt1"}],
+                "subnets": [
+                    {"name": "subnet2", "router:external": False, "enable_dhcp": True},
+                ],
+            }
+        }
+
+        def get_side_effect(url, timeout=10):
+            print(f"Called with timeout={timeout}")
+            if (
+                "/v2.0/vpcs" in url
+                and not url.endswith("vpc1")
+                and not url.endswith("vpc2")
+            ):
+                return mocked_response_vpcs
+            elif url.endswith("vpc1"):
+                return mocked_response_vpc1
+            elif url.endswith("vpc2"):
+                return mocked_response_vpc2
+            else:
+                mock_404 = MagicMock()
+                mock_404.status_code = 404
+                mock_404.text = "Not Found"
+                return mock_404
+
+        provider.session.get.side_effect = get_side_effect
+
+        # create NHNNetworkService, which internally calls _get_networks() and _get_subnets()
+        network_service = NHNNetworkService(provider)
+
+        assert len(network_service.networks) == 2
+
+        # first network
+        net1 = network_service.networks[0]
+        assert net1.id == "vpc1"
+        assert net1.name == "myvpc1"
+        assert net1.empty_routingtables is True
+        assert len(net1.subnets) == 1
+        assert net1.subnets[0].name == "subnet1"
+        assert net1.subnets[0].external_router is True
+        assert net1.subnets[0].enable_dhcp is False
+
+        # second network
+        net2 = network_service.networks[1]
+        assert net2.id == "vpc2"
+        assert net2.name == "myvpc2"
+        assert net2.empty_routingtables is False  # Assuming there's a routing table
+        assert len(net2.subnets) == 1
+        assert net2.subnets[0].name == "subnet2"
+        assert net2.subnets[0].external_router is False
+        assert net2.subnets[0].enable_dhcp is True
+
+        mock_logger.error.assert_not_called()
diff --git a/tests/providers/nhn/services/network/network_vpc_has_empty_routingtables/network_vpc_has_empty_routingtables_test_for_nhn.py b/tests/providers/nhn/services/network/network_vpc_has_empty_routingtables/network_vpc_has_empty_routingtables_test_for_nhn.py
new file mode 100644
index 0000000000..3af49eb699
--- /dev/null
+++ b/tests/providers/nhn/services/network/network_vpc_has_empty_routingtables/network_vpc_has_empty_routingtables_test_for_nhn.py
@@ -0,0 +1,107 @@
+from unittest import mock
+from uuid import uuid4
+
+from prowler.providers.nhn.services.network.network_service import Network
+from tests.providers.nhn.nhn_fixtures import set_mocked_nhn_provider
+
+
+class Test_vpc_has_empty_routingtables:
+    def test_no_networks(self):
+        # 1) Make a MagicMock for network_client
+        network_client = mock.MagicMock()
+        network_client.networks = []
+
+        # 2) Patch get_global_provider() to return a mocked NHN provider
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                # patch the 'network_empty_routingtables.network_client' used in the check code
+                "prowler.providers.nhn.services.network.network_vpc_has_empty_routingtables.network_vpc_has_empty_routingtables.network_client",
+                new=network_client,
+            ),
+        ):
+            # 3) Import the check code AFTER patching
+            from prowler.providers.nhn.services.network.network_vpc_has_empty_routingtables.network_vpc_has_empty_routingtables import (
+                network_vpc_has_empty_routingtables,
+            )
+
+            # 4) Run the check
+            check = network_vpc_has_empty_routingtables()
+            result = check.execute()
+
+            # 5) Assertions
+            assert len(result) == 0  # no networks => no findings
+
+    def test_vpc_has_empty_routingtables(self):
+        # Make a MagicMock for network_client
+        network_client = mock.MagicMock()
+
+        # Suppose we have 1 network with empty_routingtables=True => FAIL expected
+        network_id = str(uuid4())
+        network_name = "testNetwork"
+        mock_network = mock.MagicMock(spec=Network)
+        mock_network.id = network_id
+        mock_network.name = network_name
+        mock_network.empty_routingtables = True
+        network_client.networks = [mock_network]
+
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                "prowler.providers.nhn.services.network.network_vpc_has_empty_routingtables.network_vpc_has_empty_routingtables.network_client",
+                new=network_client,
+            ),
+        ):
+            from prowler.providers.nhn.services.network.network_vpc_has_empty_routingtables.network_vpc_has_empty_routingtables import (
+                network_vpc_has_empty_routingtables,
+            )
+
+            check = network_vpc_has_empty_routingtables()
+            result = check.execute()
+
+            assert len(result) == 1
+            assert result[0].status == "FAIL"
+            assert "has empty routingtables" in result[0].status_extended
+            assert result[0].resource_name == network_name
+            assert result[0].resource_id == network_id
+
+    def test_vpc_does_not_have_empty_routingtables(self):
+        # Another scenario: network with empty_routingtables=False => PASS expected
+        network_client = mock.MagicMock()
+
+        network_id = str(uuid4())
+        network_name = "testNetwork"
+        mock_network = mock.MagicMock(spec=Network)
+        mock_network.id = network_id
+        mock_network.name = network_name
+        mock_network.empty_routingtables = False
+        network_client.networks = [mock_network]
+
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                "prowler.providers.nhn.services.network.network_vpc_has_empty_routingtables.network_vpc_has_empty_routingtables.network_client",
+                new=network_client,
+            ),
+        ):
+            from prowler.providers.nhn.services.network.network_vpc_has_empty_routingtables.network_vpc_has_empty_routingtables import (
+                network_vpc_has_empty_routingtables,
+            )
+
+            check = network_vpc_has_empty_routingtables()
+            result = check.execute()
+
+            assert len(result) == 0
+            assert result[0].status == "PASS"
+            assert "dose not have empty routingtables" in result[0].status_extended
+            assert result[0].resource_name == network_name
+            assert result[0].resource_id == network_id
diff --git a/tests/providers/nhn/services/network/network_vpc_subnet_enable_dhcp/network_vpc_subnet_enable_dhcp_for_nhn.py b/tests/providers/nhn/services/network/network_vpc_subnet_enable_dhcp/network_vpc_subnet_enable_dhcp_for_nhn.py
new file mode 100644
index 0000000000..2c3c1cf722
--- /dev/null
+++ b/tests/providers/nhn/services/network/network_vpc_subnet_enable_dhcp/network_vpc_subnet_enable_dhcp_for_nhn.py
@@ -0,0 +1,113 @@
+from unittest import mock
+from uuid import uuid4
+
+from prowler.providers.nhn.services.network.network_service import Network, Subnet
+from tests.providers.nhn.nhn_fixtures import set_mocked_nhn_provider
+
+
+class Test_network_vpc_subnet_enable_dhcp:
+    def test_no_networks(self):
+        # 1) Make a MagicMock for network_client
+        network_client = mock.MagicMock()
+        network_client.networks = []
+
+        # 2) Patch get_global_provider() to return a mocked NHN provider
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                # patch the 'network_vpc_subnet_enable_dhcp.network_client' used in the check code
+                "prowler.providers.nhn.services.network.network_vpc_subnet_enable_dhcp.network_vpc_subnet_enable_dhcp.network_client",
+                new=network_client,
+            ),
+        ):
+            # 3) Import the check code AFTER patching
+            from prowler.providers.nhn.services.network.network_vpc_subnet_enable_dhcp.network_vpc_subnet_enable_dhcp import (
+                network_vpc_subnet_enable_dhcp,
+            )
+
+            # 4) Run the check
+            check = network_vpc_subnet_enable_dhcp()
+            result = check.execute()
+
+            # 5) Assertions
+            assert len(result) == 0  # no networks => no findings
+
+    def test_vpc_subnet_enable_dhcp(self):
+        # Make a MagicMock for network_client
+        network_client = mock.MagicMock()
+
+        # Suppose we have 1 network with enable_dhcp=True => FAIL expected
+        network_id = str(uuid4())
+        network_name = "testNetwork"
+        mock_network = mock.MagicMock(spec=Network)
+        mock_network.id = network_id
+        mock_network.name = network_name
+        mock_subnet = mock.MagicMock(spec=Subnet)
+        mock_subnet.name = "subnet1"
+        mock_subnet.enable_dhcp = True
+        mock_network.subnets = [mock_subnet]
+        network_client.networks = [mock_network]
+
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                "prowler.providers.nhn.services.network.network_vpc_subnet_enable_dhcp.network_vpc_subnet_enable_dhcp.network_client",
+                new=network_client,
+            ),
+        ):
+            from prowler.providers.nhn.services.network.network_vpc_subnet_enable_dhcp.network_vpc_subnet_enable_dhcp import (
+                network_vpc_subnet_enable_dhcp,
+            )
+
+            check = network_vpc_subnet_enable_dhcp()
+            result = check.execute()
+
+            assert len(result) == 1
+            assert result[0].status == "FAIL"
+            assert "has DHCP enabled" in result[0].status_extended
+            assert result[0].resource_name == network_name
+            assert result[0].resource_id == network_id
+
+    def test_vpc_subnet_unable_dhcp(self):
+        # Another scenario: network with enable_dhcp=False => PASS expected
+        network_client = mock.MagicMock()
+
+        network_id = str(uuid4())
+        network_name = "testNetwork"
+        mock_network = mock.MagicMock(spec=Network)
+        mock_network.id = network_id
+        mock_network.name = network_name
+        mock_subnet = mock.MagicMock(spec=Subnet)
+        mock_subnet.name = "subnet1"
+        mock_subnet.enable_dhcp = False
+        mock_network.subnets = [mock_subnet]
+        network_client.networks = [mock_network]
+
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                "prowler.providers.nhn.services.network.network_vpc_subnet_enable_dhcp.network_vpc_subnet_enable_dhcp.network_client",
+                new=network_client,
+            ),
+        ):
+            from prowler.providers.nhn.services.network.network_vpc_subnet_enable_dhcp.network_vpc_subnet_enable_dhcp import (
+                network_vpc_subnet_enable_dhcp,
+            )
+
+            check = network_vpc_subnet_enable_dhcp()
+            result = check.execute()
+
+            assert len(result) == 1
+            assert result[0].status == "PASS"
+            assert "does not have DHCP enabled" in result[0].status_extended
+            assert result[0].resource_name == network_name
+            assert result[0].resource_id == network_id
diff --git a/tests/providers/nhn/services/network/network_vpc_subnet_has_external_router/network_vpc_subnet_has_external_router_for_nhn.py b/tests/providers/nhn/services/network/network_vpc_subnet_has_external_router/network_vpc_subnet_has_external_router_for_nhn.py
new file mode 100644
index 0000000000..ec45a22ecd
--- /dev/null
+++ b/tests/providers/nhn/services/network/network_vpc_subnet_has_external_router/network_vpc_subnet_has_external_router_for_nhn.py
@@ -0,0 +1,104 @@
+from unittest import mock
+from uuid import uuid4
+
+from prowler.providers.nhn.services.network.network_service import Network, Subnet
+from tests.providers.nhn.nhn_fixtures import set_mocked_nhn_provider
+
+
+class Test_network_vpc_subnet_has_external_router:
+    def test_no_networks(self):
+        network_client = mock.MagicMock()
+        network_client.networks = []
+
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                "prowler.providers.nhn.services.network.network_vpc_subnet_has_external_router.network_vpc_subnet_has_external_router.network_client",
+                new=network_client,
+            ),
+        ):
+            from prowler.providers.nhn.services.network.network_vpc_subnet_has_external_router.network_vpc_subnet_has_external_router import (
+                network_vpc_subnet_has_external_router,
+            )
+
+            check = network_vpc_subnet_has_external_router()
+            result = check.execute()
+
+            assert len(result) == 0
+
+    def test_vpc_subnet_has_external_router(self):
+        network_client = mock.MagicMock()
+
+        network_id = str(uuid4())
+        network_name = "testNetwork"
+        mock_network = mock.MagicMock(spec=Network)
+        mock_network.id = network_id
+        mock_network.name = network_name
+        mock_subnet = mock.MagicMock(spec=Subnet)
+        mock_subnet.name = "subnet1"
+        mock_subnet.external_router = True
+        mock_network.subnets = [mock_subnet]
+        network_client.networks = [mock_network]
+
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                "prowler.providers.nhn.services.network.network_vpc_subnet_has_external_router.network_vpc_subnet_has_external_router.network_client",
+                new=network_client,
+            ),
+        ):
+            from prowler.providers.nhn.services.network.network_vpc_subnet_has_external_router.network_vpc_subnet_has_external_router import (
+                network_vpc_subnet_has_external_router,
+            )
+
+            check = network_vpc_subnet_has_external_router()
+            result = check.execute()
+
+            assert len(result) == 1
+            assert result[0].status == "FAIL"
+            assert "has external router" in result[0].status_extended
+            assert result[0].resource_id == network_id
+            assert result[0].resource_name == network_name
+
+    def test_vpc_subnet_no_external_router(self):
+        network_client = mock.MagicMock()
+
+        network_id = str(uuid4())
+        network_name = "testNetwork"
+        mock_network = mock.MagicMock(spec=Network)
+        mock_network.id = network_id
+        mock_network.name = network_name
+        mock_subnet = mock.MagicMock(spec=Subnet)
+        mock_subnet.name = "subnet1"
+        mock_subnet.external_router = False
+        mock_network.subnets = [mock_subnet]
+        network_client.networks = [mock_network]
+
+        with (
+            mock.patch(
+                "prowler.providers.common.provider.Provider.get_global_provider",
+                return_value=set_mocked_nhn_provider(),
+            ),
+            mock.patch(
+                "prowler.providers.nhn.services.network.network_vpc_subnet_has_external_router.network_vpc_subnet_has_external_router.network_client",
+                new=network_client,
+            ),
+        ):
+            from prowler.providers.nhn.services.network.network_vpc_subnet_has_external_router.network_vpc_subnet_has_external_router import (
+                network_vpc_subnet_has_external_router,
+            )
+
+            check = network_vpc_subnet_has_external_router()
+            result = check.execute()
+
+            assert len(result) == 1
+            assert result[0].status == "PASS"
+            assert "no external router" in result[0].status_extended
+            assert result[0].resource_id == network_id
+            assert result[0].resource_name == network_name


Code review comments:
diff_hunk: @@ -0,0 +1,33 @@
+{
+    "Provider": "nhn",
+    "CheckID": "network_vpc_subnet_enable_dhcp",
+    "CheckTitle": "Check if DHCP is enabled for subnets in VPC",
+    "CheckType": [], 
+    "ServiceName": "network",
+    "SubServiceName": "",
+    "ResourceIdTemplate": "",
+    "Risk": "",
+    "Severity": "Medium",
path: prowler/providers/nhn/services/network/network_vpc_subnet_enable_dhcp/network_vpc_subnet_enable_dhcp.metadata.json
text: @user1:
```suggestion
    "Severity": "medium",
```