mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 21:11:53 +00:00
feat(e2e): provider for e2e cloud (#11654)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
@@ -615,6 +615,30 @@ jobs:
|
||||
flags: prowler-py${{ matrix.python-version }}-linode
|
||||
files: ./linode_coverage.xml
|
||||
|
||||
# E2E Networks Provider
|
||||
- name: Check if E2E Networks files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
id: changed-e2enetworks
|
||||
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||
with:
|
||||
files: |
|
||||
./prowler/**/e2enetworks/**
|
||||
./tests/**/e2enetworks/**
|
||||
./uv.lock
|
||||
|
||||
- name: Run E2E Networks tests
|
||||
if: steps.changed-e2enetworks.outputs.any_changed == 'true'
|
||||
run: uv run pytest -n auto --cov=./prowler/providers/e2enetworks --cov-report=xml:e2enetworks_coverage.xml tests/providers/e2enetworks
|
||||
|
||||
- name: Upload E2E Networks coverage to Codecov
|
||||
if: steps.changed-e2enetworks.outputs.any_changed == 'true'
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
flags: prowler-py${{ matrix.python-version }}-e2enetworks
|
||||
files: ./e2enetworks_coverage.xml
|
||||
|
||||
# External Provider (dynamic loading)
|
||||
- name: Check if External Provider files changed
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
|
||||
@@ -141,6 +141,7 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically
|
||||
| Vercel | 26 | 6 | 1 | 8 | Official | UI, API, CLI |
|
||||
| Okta | 29 | 8 | 2 | 2 | Official | UI, API, CLI |
|
||||
| Linode [Contact us](https://prowler.com/contact) | 10 | 3 | 1 | 4 | Unofficial | CLI |
|
||||
| E2E Networks [Contact us](https://prowler.com/contact) | 27 | 6 | 0 | 2 | Unofficial | CLI |
|
||||
| Scaleway [Contact us](https://prowler.com/contact) | 1 | 1 | 1 | 1 | Unofficial | CLI |
|
||||
| StackIT [Contact us](https://prowler.com/contact) | 7 | 2 | 1 | 3 | Unofficial | CLI |
|
||||
| NHN | 6 | 2 | 2 | 0 | Unofficial | CLI |
|
||||
|
||||
@@ -378,6 +378,13 @@
|
||||
"user-guide/providers/linode/getting-started-linode",
|
||||
"user-guide/providers/linode/authentication"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "E2E Networks",
|
||||
"pages": [
|
||||
"user-guide/providers/e2enetworks/getting-started-e2enetworks",
|
||||
"user-guide/providers/e2enetworks/authentication"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -31,6 +31,7 @@ Prowler supports a wide range of providers organized by category:
|
||||
| [AWS](/user-guide/providers/aws/getting-started-aws) | Official | Accounts | UI, API, CLI |
|
||||
| [Azure](/user-guide/providers/azure/getting-started-azure) | Official | Subscriptions | UI, API, CLI |
|
||||
| [Cloudflare](/user-guide/providers/cloudflare/getting-started-cloudflare) | Official | Accounts | UI, API, CLI |
|
||||
| [E2E Networks](/user-guide/providers/e2enetworks/getting-started-e2enetworks) | [Contact us](https://prowler.com/contact) | Projects | CLI |
|
||||
| [Google Cloud](/user-guide/providers/gcp/getting-started-gcp) | Official | Projects | UI, API, CLI |
|
||||
| [Linode](/user-guide/providers/linode/getting-started-linode) | [Contact us](https://prowler.com/contact) | Accounts | CLI |
|
||||
| **NHN** | [Contact us](https://prowler.com/contact) | Tenants | CLI |
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: "E2E Networks Authentication in Prowler"
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="5.34.0" />
|
||||
|
||||
Prowler for E2E Networks authenticates against the E2E Networks MyAccount API using an **API key**, an **auth token**, and a numeric **project ID**. Set the API key and auth token as environment variables to avoid exposing secrets in shell history or process listings. Compatibility CLI flags are still accepted, but Prowler warns when secret values are passed directly on the command line.
|
||||
|
||||
## Required Credentials
|
||||
|
||||
Prowler requires read access to the E2E Networks project. The following values are needed:
|
||||
|
||||
| Credential | Environment Variable | Description |
|
||||
|------------|----------------------|-------------|
|
||||
| API Key | `E2E_NETWORKS_API_KEY` | Identifies the MyAccount API key used for requests |
|
||||
| Auth Token | `E2E_NETWORKS_AUTH_TOKEN` | Bearer token authorizing the API key |
|
||||
| Project ID | `E2E_NETWORKS_PROJECT_ID` | Numeric ID of the project to scan |
|
||||
| Region | `E2E_NETWORKS_REGION` | Optional region to scan (defaults to all regions: Delhi, Chennai) |
|
||||
|
||||
<Warning>
|
||||
The project ID must be an integer. A missing or non-numeric project ID stops the scan before any resource is read.
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
## API Credentials
|
||||
|
||||
### Step 1: Create the API Key and Auth Token
|
||||
|
||||
1. Log into the [E2E Networks MyAccount portal](https://myaccount.e2enetworks.com).
|
||||
2. Open the **API Tokens** section under the account settings.
|
||||
3. Create a new API token and copy both the API key and the auth token.
|
||||
4. Copy the values immediately — the auth token is not shown again.
|
||||
|
||||
### Step 2: Find the Project ID
|
||||
|
||||
Select the target project in MyAccount and copy its numeric project ID from the project settings or the API context.
|
||||
|
||||
### Step 3: Configure Authentication
|
||||
|
||||
Export the credentials as environment variables:
|
||||
|
||||
```bash
|
||||
export E2E_NETWORKS_API_KEY="your-api-key"
|
||||
export E2E_NETWORKS_AUTH_TOKEN="your-auth-token"
|
||||
export E2E_NETWORKS_PROJECT_ID="your-project-id"
|
||||
```
|
||||
|
||||
Then run Prowler:
|
||||
|
||||
```bash
|
||||
prowler e2enetworks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verifying Authentication
|
||||
|
||||
To confirm that Prowler can reach the E2E Networks project, run a scan against a single location:
|
||||
|
||||
```bash
|
||||
prowler e2enetworks --region Delhi
|
||||
```
|
||||
|
||||
A successful run reports findings for the discovered resources. A failed run displays an error message indicating the credential or connectivity issue.
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
For automated pipelines, set the credentials as secret environment variables:
|
||||
|
||||
**GitHub Actions:**
|
||||
|
||||
```yaml
|
||||
env:
|
||||
E2E_NETWORKS_API_KEY: ${{ secrets.E2E_NETWORKS_API_KEY }}
|
||||
E2E_NETWORKS_AUTH_TOKEN: ${{ secrets.E2E_NETWORKS_AUTH_TOKEN }}
|
||||
E2E_NETWORKS_PROJECT_ID: ${{ secrets.E2E_NETWORKS_PROJECT_ID }}
|
||||
|
||||
steps:
|
||||
- name: Run Prowler
|
||||
run: prowler e2enetworks
|
||||
```
|
||||
|
||||
**GitLab CI:**
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
E2E_NETWORKS_API_KEY: $E2E_NETWORKS_API_KEY
|
||||
E2E_NETWORKS_AUTH_TOKEN: $E2E_NETWORKS_AUTH_TOKEN
|
||||
E2E_NETWORKS_PROJECT_ID: $E2E_NETWORKS_PROJECT_ID
|
||||
|
||||
prowler_scan:
|
||||
script:
|
||||
- prowler e2enetworks
|
||||
```
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: 'Getting Started With E2E Networks on Prowler'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="5.34.0" />
|
||||
|
||||
Prowler for E2E Networks scans E2E Networks infrastructure for security misconfigurations across compute nodes, networking, security groups, load balancers, storage, and managed databases.
|
||||
|
||||
<Note>
|
||||
E2E Networks support in Prowler is community-maintained. For commercial support or to request additional service coverage, [contact us](https://prowler.com/contact).
|
||||
</Note>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Set up authentication for E2E Networks with the [E2E Networks Authentication](/user-guide/providers/e2enetworks/authentication) guide before starting:
|
||||
|
||||
- Create an E2E Networks API key and auth token from the MyAccount API Tokens section
|
||||
- Note the numeric project ID of the project to scan
|
||||
|
||||
## Prowler CLI
|
||||
|
||||
### Run Prowler for E2E Networks
|
||||
|
||||
Once authenticated, export the credentials as environment variables and run Prowler for E2E Networks. Environment variables keep secrets out of shell history and process listings:
|
||||
|
||||
```bash
|
||||
export E2E_NETWORKS_API_KEY="your-api-key"
|
||||
export E2E_NETWORKS_AUTH_TOKEN="your-auth-token"
|
||||
export E2E_NETWORKS_PROJECT_ID="your-project-id"
|
||||
prowler e2enetworks
|
||||
```
|
||||
|
||||
### Run Specific Checks
|
||||
|
||||
```bash
|
||||
prowler e2enetworks --checks node_encryption_enabled storage_block_volume_not_orphaned
|
||||
```
|
||||
|
||||
### Run a Specific Service
|
||||
|
||||
```bash
|
||||
prowler e2enetworks --services node
|
||||
```
|
||||
|
||||
### Scan Specific Regions
|
||||
|
||||
Use `--region` (aliases `--filter-region` and `-f`) to limit the scan to one or more E2E Networks regions. When the flag is omitted, Prowler reads `E2E_NETWORKS_REGION` and otherwise scans all regions (Delhi, Chennai).
|
||||
|
||||
```bash
|
||||
prowler e2enetworks --region Delhi Chennai
|
||||
```
|
||||
|
||||
## Available Services
|
||||
|
||||
Prowler for E2E Networks currently supports the following services:
|
||||
|
||||
| Service | Description |
|
||||
|---------|-------------|
|
||||
| `database` | Managed database clusters and their backup, network, and encryption settings |
|
||||
| `loadbalancer` | Application load balancers and their TLS, health-check, and protection settings |
|
||||
| `network` | Virtual Private Clouds, peering, and reserved public IP addresses |
|
||||
| `node` | Compute nodes and their encryption, protection, and network configuration |
|
||||
| `securitygroup` | Security groups and their inbound and default traffic rules |
|
||||
| `storage` | Block volumes and file storage (EFS/EPFS) with their protection settings |
|
||||
@@ -140,6 +140,7 @@ from prowler.providers.azure.models import AzureOutputOptions
|
||||
from prowler.providers.cloudflare.models import CloudflareOutputOptions
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.common.quick_inventory import run_provider_quick_inventory
|
||||
from prowler.providers.e2enetworks.models import E2eNetworksOutputOptions
|
||||
from prowler.providers.gcp.models import GCPOutputOptions
|
||||
from prowler.providers.github.models import GithubOutputOptions
|
||||
from prowler.providers.googleworkspace.models import GoogleWorkspaceOutputOptions
|
||||
@@ -432,6 +433,10 @@ def prowler():
|
||||
output_options = VercelOutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
)
|
||||
elif provider == "e2enetworks":
|
||||
output_options = E2eNetworksOutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
)
|
||||
elif provider == "okta":
|
||||
output_options = OktaOutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
E2E Networks provider with 27 checks across compute nodes, networking, security groups, load balancers, block/file storage, and managed databases
|
||||
@@ -81,6 +81,7 @@ class Provider(str, Enum):
|
||||
OKTA = "okta"
|
||||
STACKIT = "stackit"
|
||||
LINODE = "linode"
|
||||
E2ENETWORKS = "e2enetworks"
|
||||
|
||||
|
||||
# Compliance
|
||||
|
||||
@@ -801,3 +801,8 @@ openstack:
|
||||
# Regex patterns whose matches are excluded from secret scanning of
|
||||
# resource metadata.
|
||||
secrets_ignore_patterns: []
|
||||
|
||||
# E2E Networks Configuration
|
||||
e2enetworks:
|
||||
# load_balancer_bitninja_enabled
|
||||
require_bitninja_on_load_balancers: false
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
### Account, Check and/or Region can be * to apply for all the cases.
|
||||
### Account == * (E2E Networks findings are matched by resource, so use * to apply to all)
|
||||
### Region == <E2E Networks location, e.g. Delhi> or * for all locations
|
||||
### 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:
|
||||
"*":
|
||||
Checks:
|
||||
"node_public_ip_not_assigned":
|
||||
Regions:
|
||||
- "Delhi"
|
||||
Resources:
|
||||
- "example-node-id"
|
||||
- "bastion-.*"
|
||||
"storage_bucket_public_access_disabled":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "public-assets-bucket"
|
||||
@@ -0,0 +1,22 @@
|
||||
"""E2E Networks provider config schema."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from prowler.config.schema.base import ProviderConfigBase
|
||||
|
||||
|
||||
class E2eNetworksProviderConfig(ProviderConfigBase):
|
||||
"""E2E Networks provider configuration schema.
|
||||
|
||||
Defines optional configuration parameters for E2E Networks security checks.
|
||||
"""
|
||||
|
||||
require_bitninja_on_load_balancers: Optional[bool] = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Whether BitNinja protection is required on load balancers for the "
|
||||
"loadbalancer_bitninja_enabled check."
|
||||
),
|
||||
)
|
||||
@@ -9,6 +9,7 @@ from prowler.config.schema.aws import AWSProviderConfig
|
||||
from prowler.config.schema.azure import AzureProviderConfig
|
||||
from prowler.config.schema.base import ProviderConfigBase
|
||||
from prowler.config.schema.cloudflare import CloudflareProviderConfig
|
||||
from prowler.config.schema.e2enetworks import E2eNetworksProviderConfig
|
||||
from prowler.config.schema.gcp import GCPProviderConfig
|
||||
from prowler.config.schema.github import GitHubProviderConfig
|
||||
from prowler.config.schema.kubernetes import KubernetesProviderConfig
|
||||
@@ -27,6 +28,7 @@ SCHEMAS: dict[str, type[ProviderConfigBase]] = {
|
||||
"github": GitHubProviderConfig,
|
||||
"mongodbatlas": MongoDBAtlasProviderConfig,
|
||||
"cloudflare": CloudflareProviderConfig,
|
||||
"e2enetworks": E2eNetworksProviderConfig,
|
||||
"vercel": VercelProviderConfig,
|
||||
"okta": OktaProviderConfig,
|
||||
"alibabacloud": AlibabaCloudProviderConfig,
|
||||
|
||||
@@ -1286,6 +1286,29 @@ class CheckReportNHN(Check_Report):
|
||||
self.location = getattr(resource, "location", "kr1")
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckReportE2eNetworks(Check_Report):
|
||||
"""Contains the E2E Networks Check's finding information."""
|
||||
|
||||
resource_name: str
|
||||
resource_id: str
|
||||
location: str
|
||||
|
||||
def __init__(self, metadata: Dict, resource: Any) -> None:
|
||||
"""Initialize the E2E Networks Check's finding information.
|
||||
|
||||
Args:
|
||||
metadata: The metadata of the check.
|
||||
resource: Basic information about the E2E Networks resource.
|
||||
"""
|
||||
super().__init__(metadata, resource)
|
||||
self.resource_name = getattr(
|
||||
resource, "name", getattr(resource, "resource_name", "")
|
||||
)
|
||||
self.resource_id = getattr(resource, "id", getattr(resource, "resource_id", ""))
|
||||
self.location = getattr(resource, "location", "global")
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckReportStackIT(Check_Report):
|
||||
"""Contains the StackIT Check's finding information."""
|
||||
|
||||
@@ -48,6 +48,7 @@ class ProwlerArgumentParser:
|
||||
"nhn",
|
||||
"mongodbatlas",
|
||||
"vercel",
|
||||
"e2enetworks",
|
||||
"okta",
|
||||
"scaleway",
|
||||
"stackit",
|
||||
@@ -74,10 +75,10 @@ class ProwlerArgumentParser:
|
||||
self.parser = argparse.ArgumentParser(
|
||||
prog="prowler",
|
||||
formatter_class=RawTextHelpFormatter,
|
||||
usage=f"prowler [-h] [--version] {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,linode,dashboard,iac,image,llm{extra_providers_csv}}} ...",
|
||||
usage=f"prowler [-h] [--version] {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,linode,e2enetworks,dashboard,iac,image,llm{extra_providers_csv}}} ...",
|
||||
epilog=f"""
|
||||
Available Cloud Providers:
|
||||
{{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,linode{extra_providers_csv}}}
|
||||
{{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,linode,e2enetworks{extra_providers_csv}}}
|
||||
aws AWS Provider
|
||||
azure Azure Provider
|
||||
gcp GCP Provider
|
||||
@@ -98,7 +99,8 @@ Available Cloud Providers:
|
||||
mongodbatlas MongoDB Atlas Provider
|
||||
scaleway Scaleway Provider
|
||||
vercel Vercel Provider
|
||||
linode Linode Provider{extra_providers_text}
|
||||
linode Linode Provider
|
||||
e2enetworks E2E Networks Provider{extra_providers_text}
|
||||
|
||||
|
||||
Available components:
|
||||
|
||||
@@ -26,6 +26,7 @@ PROVIDER_HEADER_MAP = {
|
||||
"oraclecloud": ("TenancyId", "account_uid", "Region", "region"),
|
||||
"alibabacloud": ("AccountId", "account_uid", "Region", "region"),
|
||||
"nhn": ("AccountId", "account_uid", "Region", "region"),
|
||||
"e2enetworks": ("ProjectId", "account_uid", "Location", "region"),
|
||||
}
|
||||
_DEFAULT_HEADERS = ("AccountId", "account_uid", "Region", "region")
|
||||
|
||||
|
||||
@@ -342,6 +342,18 @@ class Finding(BaseModel):
|
||||
output_data["resource_uid"] = check_output.resource_id
|
||||
output_data["region"] = check_output.location
|
||||
|
||||
elif provider.type == "e2enetworks":
|
||||
output_data["auth_method"] = "api_key_and_bearer_token"
|
||||
output_data["account_uid"] = str(
|
||||
get_nested_attribute(provider, "identity.project_id")
|
||||
)
|
||||
output_data["account_name"] = str(
|
||||
get_nested_attribute(provider, "identity.project_id")
|
||||
)
|
||||
output_data["resource_name"] = check_output.resource_name
|
||||
output_data["resource_uid"] = check_output.resource_id
|
||||
output_data["region"] = check_output.location
|
||||
|
||||
elif provider.type == "stackit":
|
||||
output_data["auth_method"] = getattr(
|
||||
provider, "auth_method", "api_token"
|
||||
|
||||
@@ -2,6 +2,7 @@ import sys
|
||||
from io import TextIOWrapper
|
||||
|
||||
import markdown
|
||||
from markupsafe import escape
|
||||
|
||||
from prowler.config.config import (
|
||||
html_logo_url,
|
||||
@@ -1396,6 +1397,46 @@ class HTML(Output):
|
||||
)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_e2enetworks_assessment_summary(provider: Provider) -> str:
|
||||
"""Get the HTML assessment summary for the E2E Networks provider."""
|
||||
try:
|
||||
locations = escape(", ".join(provider.identity.locations))
|
||||
project_id = escape(str(provider.identity.project_id))
|
||||
return f"""
|
||||
<div class="col-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
E2E Networks Assessment Summary
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<b>Project ID:</b> {project_id}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<b>Locations:</b> {locations}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
E2E Networks Credentials
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<b>Authentication:</b> API Key + Bearer Token
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>"""
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_vercel_assessment_summary(provider: Provider) -> str:
|
||||
"""
|
||||
|
||||
@@ -24,6 +24,8 @@ def stdout_report(finding, color, verbose, status, fix, provider=None):
|
||||
details = finding.location
|
||||
elif finding.check_metadata.Provider == "nhn":
|
||||
details = finding.location
|
||||
elif finding.check_metadata.Provider == "e2enetworks":
|
||||
details = finding.location
|
||||
elif finding.check_metadata.Provider == "stackit":
|
||||
details = finding.location
|
||||
elif finding.check_metadata.Provider == "llm":
|
||||
|
||||
@@ -70,6 +70,9 @@ def display_summary_table(
|
||||
elif provider.type == "nhn":
|
||||
entity_type = "Tenant Domain"
|
||||
audited_entities = provider.identity.tenant_domain
|
||||
elif provider.type == "e2enetworks":
|
||||
entity_type = "Project"
|
||||
audited_entities = str(provider.identity.project_id)
|
||||
elif provider.type == "stackit":
|
||||
if provider.identity.project_name:
|
||||
entity_type = "Project"
|
||||
|
||||
@@ -625,6 +625,16 @@ class Provider(ABC):
|
||||
mutelist_path=arguments.mutelist_file,
|
||||
fixer_config=fixer_config,
|
||||
)
|
||||
elif arguments.provider == "e2enetworks":
|
||||
provider_class(
|
||||
api_key=getattr(arguments, "e2e_networks_api_key", None),
|
||||
auth_token=getattr(arguments, "e2e_networks_auth_token", None),
|
||||
project_id=getattr(arguments, "e2e_networks_project_id", None),
|
||||
locations=getattr(arguments, "region", None),
|
||||
config_path=arguments.config_file,
|
||||
mutelist_path=arguments.mutelist_file,
|
||||
fixer_config=fixer_config,
|
||||
)
|
||||
elif arguments.provider == "okta":
|
||||
provider_class(
|
||||
okta_org_domain=getattr(arguments, "okta_org_domain", ""),
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import os
|
||||
|
||||
import requests
|
||||
from colorama import Fore, 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.e2enetworks.exceptions.exceptions import (
|
||||
E2eNetworksCredentialsError,
|
||||
E2eNetworksSessionError,
|
||||
)
|
||||
from prowler.providers.e2enetworks.lib.mutelist.mutelist import E2eNetworksMutelist
|
||||
from prowler.providers.e2enetworks.models import (
|
||||
E2E_DEFAULT_LOCATIONS,
|
||||
E2eNetworksIdentityInfo,
|
||||
E2eNetworksSession,
|
||||
)
|
||||
|
||||
|
||||
class E2enetworksProvider(Provider):
|
||||
"""Provider class for E2E Networks."""
|
||||
|
||||
_type: str = "e2enetworks"
|
||||
_session: E2eNetworksSession
|
||||
_identity: E2eNetworksIdentityInfo
|
||||
_audit_config: dict
|
||||
_fixer_config: dict
|
||||
_mutelist: E2eNetworksMutelist
|
||||
audit_metadata: Audit_Metadata
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str = None,
|
||||
auth_token: str = None,
|
||||
project_id: str | int = None,
|
||||
locations: list[str] | None = None,
|
||||
config_path: str = None,
|
||||
fixer_config: dict = None,
|
||||
mutelist_path: str = None,
|
||||
mutelist_content: dict = None,
|
||||
):
|
||||
logger.info("Initializing E2E Networks Provider...")
|
||||
|
||||
self._api_key = api_key or os.getenv("E2E_NETWORKS_API_KEY")
|
||||
self._auth_token = auth_token or os.getenv("E2E_NETWORKS_AUTH_TOKEN")
|
||||
project_value = project_id or os.getenv("E2E_NETWORKS_PROJECT_ID")
|
||||
self._project_id = int(project_value) if project_value else None
|
||||
self._locations = self._resolve_locations(locations)
|
||||
|
||||
if not self._api_key or not self._auth_token or self._project_id is None:
|
||||
raise E2eNetworksCredentialsError(
|
||||
message="E2enetworksProvider requires api_key, auth_token, and project_id."
|
||||
)
|
||||
|
||||
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 is not None:
|
||||
self._mutelist = E2eNetworksMutelist(mutelist_content=mutelist_content)
|
||||
else:
|
||||
if not mutelist_path:
|
||||
mutelist_path = get_default_mute_file_path(self._type)
|
||||
self._mutelist = E2eNetworksMutelist(mutelist_path=mutelist_path)
|
||||
|
||||
self._session = E2enetworksProvider.setup_session(
|
||||
api_key=self._api_key,
|
||||
auth_token=self._auth_token,
|
||||
project_id=self._project_id,
|
||||
locations=self._locations,
|
||||
)
|
||||
self._identity = E2eNetworksIdentityInfo(
|
||||
project_id=self._project_id,
|
||||
locations=self._locations,
|
||||
)
|
||||
|
||||
Provider.set_global_provider(self)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_locations(locations: list[str] | None) -> list[str]:
|
||||
"""Resolve scan locations from CLI args, env vars, or defaults.
|
||||
|
||||
Args:
|
||||
locations: Optional list of location names from CLI arguments.
|
||||
|
||||
Returns:
|
||||
The resolved list of E2E Networks locations to scan.
|
||||
"""
|
||||
if locations:
|
||||
return locations
|
||||
|
||||
env_region = os.getenv("E2E_NETWORKS_REGION")
|
||||
if env_region:
|
||||
return [env_region]
|
||||
|
||||
return list(E2E_DEFAULT_LOCATIONS)
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def session(self) -> E2eNetworksSession:
|
||||
return self._session
|
||||
|
||||
@property
|
||||
def identity(self) -> E2eNetworksIdentityInfo:
|
||||
return self._identity
|
||||
|
||||
@property
|
||||
def audit_config(self) -> dict:
|
||||
return self._audit_config
|
||||
|
||||
@property
|
||||
def fixer_config(self) -> dict:
|
||||
return self._fixer_config
|
||||
|
||||
@property
|
||||
def mutelist(self) -> E2eNetworksMutelist:
|
||||
return self._mutelist
|
||||
|
||||
@staticmethod
|
||||
def setup_session(
|
||||
api_key: str,
|
||||
auth_token: str,
|
||||
project_id: int,
|
||||
locations: list[str],
|
||||
) -> E2eNetworksSession:
|
||||
"""Create an authenticated E2E Networks API session.
|
||||
|
||||
Args:
|
||||
api_key: E2E Networks API key.
|
||||
auth_token: Bearer auth token for the MyAccount API.
|
||||
project_id: E2E Networks project identifier.
|
||||
locations: Locations included in the session scope.
|
||||
|
||||
Returns:
|
||||
A configured E2eNetworksSession with an HTTP client.
|
||||
|
||||
Raises:
|
||||
E2eNetworksSessionError: If session initialization fails.
|
||||
"""
|
||||
try:
|
||||
http_session = requests.Session()
|
||||
http_session.headers.update(
|
||||
{
|
||||
"Authorization": f"Bearer {auth_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
)
|
||||
return E2eNetworksSession(
|
||||
api_key=api_key,
|
||||
auth_token=auth_token,
|
||||
project_id=project_id,
|
||||
locations=locations,
|
||||
http_session=http_session,
|
||||
)
|
||||
except Exception as error:
|
||||
raise E2eNetworksSessionError(
|
||||
message="Failed to initialize E2E Networks session.",
|
||||
original_exception=error,
|
||||
) from error
|
||||
|
||||
def print_credentials(self) -> None:
|
||||
"""Print the E2E Networks scan scope to stdout.
|
||||
|
||||
The API key and auth token are never printed, matching the behavior of
|
||||
the other Prowler providers, which only report identity and scope.
|
||||
"""
|
||||
report_lines = [
|
||||
f" Authentication: {Fore.YELLOW}API Key{Style.RESET_ALL}",
|
||||
f" Project ID: {self._project_id}",
|
||||
f" Locations: {', '.join(self._locations)}",
|
||||
]
|
||||
report_title = (
|
||||
f"{Style.BRIGHT}Using the E2E Networks credentials below:{Style.RESET_ALL}"
|
||||
)
|
||||
print_boxes(report_lines, report_title)
|
||||
|
||||
@staticmethod
|
||||
def test_connection(
|
||||
api_key: str = None,
|
||||
auth_token: str = None,
|
||||
project_id: str | int = None,
|
||||
locations: list[str] | None = None,
|
||||
raise_on_exception: bool = True,
|
||||
) -> Connection:
|
||||
"""Test connectivity to the E2E Networks MyAccount API.
|
||||
|
||||
Args:
|
||||
api_key: E2E Networks API key. Falls back to E2E_NETWORKS_API_KEY.
|
||||
auth_token: Bearer auth token. Falls back to E2E_NETWORKS_AUTH_TOKEN.
|
||||
project_id: Project identifier. Falls back to E2E_NETWORKS_PROJECT_ID.
|
||||
locations: Optional locations to use for the probe request.
|
||||
raise_on_exception: Whether to re-raise caught exceptions.
|
||||
|
||||
Returns:
|
||||
Connection indicating success or containing the error.
|
||||
|
||||
Raises:
|
||||
E2eNetworksCredentialsError: If required credentials are missing.
|
||||
E2eNetworksSessionError: If the API returns a non-200 response.
|
||||
Exception: Any unexpected error when raise_on_exception is True.
|
||||
"""
|
||||
try:
|
||||
api_key = api_key or os.getenv("E2E_NETWORKS_API_KEY")
|
||||
auth_token = auth_token or os.getenv("E2E_NETWORKS_AUTH_TOKEN")
|
||||
project_value = project_id or os.getenv("E2E_NETWORKS_PROJECT_ID")
|
||||
project_id_int = int(project_value) if project_value else None
|
||||
resolved_locations = locations or (
|
||||
[os.getenv("E2E_NETWORKS_REGION")]
|
||||
if os.getenv("E2E_NETWORKS_REGION")
|
||||
else list(E2E_DEFAULT_LOCATIONS)
|
||||
)
|
||||
|
||||
if not api_key or not auth_token or project_id_int is None:
|
||||
raise E2eNetworksCredentialsError(
|
||||
message="E2E Networks test_connection requires api_key, auth_token, and project_id."
|
||||
)
|
||||
|
||||
session = E2enetworksProvider.setup_session(
|
||||
api_key=api_key,
|
||||
auth_token=auth_token,
|
||||
project_id=project_id_int,
|
||||
locations=resolved_locations,
|
||||
)
|
||||
response = session.http_session.get(
|
||||
f"{session.base_url}/nodes/",
|
||||
params={
|
||||
"apikey": session.api_key,
|
||||
"project_id": session.project_id,
|
||||
"location": resolved_locations[0],
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
error_msg = (
|
||||
f"E2E Networks connection failed with status {response.status_code}: "
|
||||
f"{response.text}"
|
||||
)
|
||||
raise E2eNetworksSessionError(message=error_msg)
|
||||
|
||||
return Connection(is_connected=True)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise error
|
||||
return Connection(error=error)
|
||||
@@ -0,0 +1,93 @@
|
||||
from prowler.exceptions.exceptions import ProwlerException
|
||||
|
||||
|
||||
# Exceptions codes from 19000 to 19999 are reserved for E2E Networks exceptions
|
||||
class E2eNetworksBaseException(ProwlerException):
|
||||
"""Base class for E2E Networks errors."""
|
||||
|
||||
E2E_NETWORKS_ERROR_CODES = {
|
||||
(19000, "E2eNetworksCredentialsError"): {
|
||||
"message": "E2E Networks credentials not found or invalid",
|
||||
"remediation": "Provide a valid API key, auth token and project id via the E2E_NETWORKS_API_KEY, E2E_NETWORKS_AUTH_TOKEN and E2E_NETWORKS_PROJECT_ID environment variables.",
|
||||
},
|
||||
(19001, "E2eNetworksAuthenticationError"): {
|
||||
"message": "E2E Networks authentication failed",
|
||||
"remediation": "Verify the E2E Networks API key and auth token and ensure the project id is correct and accessible.",
|
||||
},
|
||||
(19002, "E2eNetworksSessionError"): {
|
||||
"message": "E2E Networks session setup failed",
|
||||
"remediation": "Review the E2E Networks credentials and network connectivity to the MyAccount API.",
|
||||
},
|
||||
(19003, "E2eNetworksAPIError"): {
|
||||
"message": "E2E Networks API request failed",
|
||||
"remediation": "Check the E2E Networks MyAccount API status and that the credentials grant access to the requested resource.",
|
||||
},
|
||||
(19004, "E2eNetworksIdentityError"): {
|
||||
"message": "Unable to retrieve E2E Networks identity or project information",
|
||||
"remediation": "Ensure the credentials allow access to the E2E Networks project and account APIs.",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
provider = "E2E"
|
||||
error_info = self.E2E_NETWORKS_ERROR_CODES.get((code, self.__class__.__name__))
|
||||
if error_info is None:
|
||||
error_info = {
|
||||
"message": message or "Unknown E2E Networks error",
|
||||
"remediation": "Check the E2E Networks MyAccount API documentation for more details.",
|
||||
}
|
||||
elif message:
|
||||
error_info = error_info.copy()
|
||||
error_info["message"] = message
|
||||
super().__init__(
|
||||
code=code,
|
||||
source=provider,
|
||||
file=file,
|
||||
original_exception=original_exception,
|
||||
error_info=error_info,
|
||||
)
|
||||
|
||||
|
||||
class E2eNetworksCredentialsError(E2eNetworksBaseException):
|
||||
"""Exception for E2E Networks credential errors."""
|
||||
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
19000, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class E2eNetworksAuthenticationError(E2eNetworksBaseException):
|
||||
"""Exception for E2E Networks authentication errors."""
|
||||
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
19001, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class E2eNetworksSessionError(E2eNetworksBaseException):
|
||||
"""Exception for E2E Networks session setup errors."""
|
||||
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
19002, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class E2eNetworksAPIError(E2eNetworksBaseException):
|
||||
"""Exception for E2E Networks API request errors."""
|
||||
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
19003, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class E2eNetworksIdentityError(E2eNetworksBaseException):
|
||||
"""Exception for E2E Networks identity errors."""
|
||||
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
19004, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.e2enetworks.exceptions.exceptions import E2eNetworksAPIError
|
||||
from prowler.providers.e2enetworks.models import E2eNetworksSession
|
||||
|
||||
|
||||
def _redact_sensitive_values(message: str) -> str:
|
||||
"""Redact E2E Networks secrets from request error messages."""
|
||||
redacted = re.sub(r"(?i)(apikey=)[^&\s]+", r"\1REDACTED", message)
|
||||
return re.sub(
|
||||
r"(?i)(Authorization:\s*Bearer\s+|Bearer\s+)[^\s,;]+",
|
||||
r"\1REDACTED",
|
||||
redacted,
|
||||
)
|
||||
|
||||
|
||||
class E2eNetworksAPIClient:
|
||||
"""Shared HTTP client for E2E Networks MyAccount API requests."""
|
||||
|
||||
def __init__(self, session: E2eNetworksSession):
|
||||
self.session = session
|
||||
|
||||
def _auth_params(self, location: str, extra: dict | None = None) -> dict[str, Any]:
|
||||
params = {
|
||||
"apikey": self.session.api_key,
|
||||
"project_id": self.session.project_id,
|
||||
"location": location,
|
||||
}
|
||||
if extra:
|
||||
params.update(extra)
|
||||
return params
|
||||
|
||||
def get(
|
||||
self,
|
||||
path: str,
|
||||
location: str,
|
||||
params: dict | None = None,
|
||||
timeout: int = 30,
|
||||
) -> dict:
|
||||
"""Perform a GET request against the E2E Networks API."""
|
||||
url = f"{self.session.base_url}{path}"
|
||||
query_params = self._auth_params(location, params)
|
||||
try:
|
||||
response = self.session.http_session.get(
|
||||
url,
|
||||
params=query_params,
|
||||
timeout=timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
if not isinstance(payload, dict):
|
||||
return {"data": payload}
|
||||
return payload
|
||||
except requests.exceptions.RequestException as error:
|
||||
redacted_error = _redact_sensitive_values(str(error))
|
||||
logger.error(
|
||||
f"E2E API GET {path} failed: {error.__class__.__name__}: {redacted_error}"
|
||||
)
|
||||
raise E2eNetworksAPIError(
|
||||
message=f"GET {path} failed for location {location}",
|
||||
original_exception=Exception(redacted_error),
|
||||
) from error
|
||||
|
||||
def get_data(
|
||||
self,
|
||||
path: str,
|
||||
location: str,
|
||||
params: dict | None = None,
|
||||
) -> list | dict:
|
||||
"""Return the `data` field from a standard E2E API envelope."""
|
||||
payload = self.get(path, location=location, params=params)
|
||||
return payload.get("data", [])
|
||||
|
||||
def paginate(
|
||||
self,
|
||||
path: str,
|
||||
location: str,
|
||||
params: dict | None = None,
|
||||
per_page: int = 100,
|
||||
) -> list:
|
||||
"""Iterate page_no/per_page style paginated list endpoints."""
|
||||
all_items: list = []
|
||||
page_no = 1
|
||||
total_pages = 1
|
||||
|
||||
while page_no <= total_pages:
|
||||
page_params = {"page_no": page_no, "per_page": per_page}
|
||||
if params:
|
||||
page_params.update(params)
|
||||
|
||||
payload = self.get(path, location=location, params=page_params)
|
||||
data = payload.get("data", [])
|
||||
if isinstance(data, list):
|
||||
all_items.extend(data)
|
||||
elif isinstance(data, dict):
|
||||
all_items.extend(data.values())
|
||||
|
||||
total_pages = int(payload.get("total_page_number", page_no))
|
||||
if not data:
|
||||
break
|
||||
page_no += 1
|
||||
|
||||
return all_items
|
||||
@@ -0,0 +1,68 @@
|
||||
import os
|
||||
|
||||
SENSITIVE_ARGUMENTS = frozenset({"--e2e-networks-api-key", "--e2e-networks-auth-token"})
|
||||
|
||||
|
||||
def init_parser(self):
|
||||
"""Init the E2E Networks Provider CLI parser."""
|
||||
e2enetworks_parser = self.subparsers.add_parser(
|
||||
"e2enetworks",
|
||||
parents=[self.common_providers_parser],
|
||||
help="E2E Networks Provider",
|
||||
)
|
||||
|
||||
auth_group = e2enetworks_parser.add_argument_group("Authentication")
|
||||
auth_group.add_argument(
|
||||
"--e2e-networks-api-key",
|
||||
nargs="?",
|
||||
default=None,
|
||||
metavar="E2E_NETWORKS_API_KEY",
|
||||
help="E2E Networks API key. Use E2E_NETWORKS_API_KEY env var instead of passing directly.",
|
||||
)
|
||||
auth_group.add_argument(
|
||||
"--e2e-networks-auth-token",
|
||||
nargs="?",
|
||||
default=None,
|
||||
metavar="E2E_NETWORKS_AUTH_TOKEN",
|
||||
help="E2E Networks auth token. Use E2E_NETWORKS_AUTH_TOKEN env var instead of passing directly.",
|
||||
)
|
||||
auth_group.add_argument(
|
||||
"--e2e-networks-project-id",
|
||||
nargs="?",
|
||||
default=None,
|
||||
metavar="E2E_NETWORKS_PROJECT_ID",
|
||||
help="E2E Networks project ID. Use E2E_NETWORKS_PROJECT_ID env var instead of passing directly.",
|
||||
)
|
||||
|
||||
regions_subparser = e2enetworks_parser.add_argument_group("Regions")
|
||||
regions_subparser.add_argument(
|
||||
"--region",
|
||||
"--filter-region",
|
||||
"-f",
|
||||
nargs="+",
|
||||
default=None,
|
||||
metavar="REGION",
|
||||
help="E2E Networks region(s) to scan (e.g. Delhi Chennai). If omitted, "
|
||||
"uses E2E_NETWORKS_REGION or scans all regions (Delhi, Chennai).",
|
||||
)
|
||||
|
||||
|
||||
def validate_arguments(arguments) -> tuple[bool, str]:
|
||||
"""Validate E2E Networks provider CLI arguments.
|
||||
|
||||
Only argument consistency is checked here so that listing operations such as
|
||||
``--list-checks`` and ``--list-services`` run without credentials. Credential
|
||||
presence (API key, auth token and project ID) is enforced later at provider
|
||||
initialization, when an actual scan is performed.
|
||||
"""
|
||||
project_id = arguments.e2e_networks_project_id or os.getenv(
|
||||
"E2E_NETWORKS_PROJECT_ID"
|
||||
)
|
||||
|
||||
if project_id is not None:
|
||||
try:
|
||||
int(project_id)
|
||||
except (TypeError, ValueError):
|
||||
return False, "E2E Networks project ID must be an integer."
|
||||
|
||||
return True, ""
|
||||
@@ -0,0 +1,27 @@
|
||||
"""E2E Networks mutelist support for suppressing findings."""
|
||||
|
||||
from prowler.lib.check.models import CheckReportE2eNetworks
|
||||
from prowler.lib.mutelist.mutelist import Mutelist
|
||||
from prowler.lib.outputs.utils import unroll_dict, unroll_tags
|
||||
|
||||
|
||||
class E2eNetworksMutelist(Mutelist):
|
||||
"""Mutelist implementation for E2E Networks check findings."""
|
||||
|
||||
def is_finding_muted(self, **kwargs) -> bool:
|
||||
"""Determine whether an E2E Networks finding is muted.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments; must include ``finding``.
|
||||
|
||||
Returns:
|
||||
True if the finding matches a mutelist entry, otherwise False.
|
||||
"""
|
||||
finding: CheckReportE2eNetworks = kwargs["finding"]
|
||||
return self.is_muted(
|
||||
finding.resource_id,
|
||||
finding.check_metadata.CheckID,
|
||||
finding.location,
|
||||
finding.resource_name,
|
||||
unroll_dict(unroll_tags(finding.resource_tags)),
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
from prowler.providers.e2enetworks.e2enetworks_provider import E2enetworksProvider
|
||||
from prowler.providers.e2enetworks.lib.api.client import E2eNetworksAPIClient
|
||||
|
||||
|
||||
class E2eNetworksService:
|
||||
"""Base class for E2E Networks services."""
|
||||
|
||||
def __init__(self, service: str, provider: E2enetworksProvider):
|
||||
"""Initialize an E2E Networks service client.
|
||||
|
||||
Args:
|
||||
service: Service name used for logging and configuration lookup.
|
||||
provider: The active E2E Networks provider instance.
|
||||
"""
|
||||
self.provider = provider
|
||||
self.audit_config = provider.audit_config
|
||||
self.fixer_config = provider.fixer_config
|
||||
self.service = service.lower() if not service.islower() else service
|
||||
self.client = E2eNetworksAPIClient(provider.session)
|
||||
@@ -0,0 +1,53 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from prowler.config.config import output_file_timestamp
|
||||
from prowler.providers.common.models import ProviderOutputOptions
|
||||
|
||||
E2E_DEFAULT_LOCATIONS = ("Delhi", "Chennai")
|
||||
E2E_BASE_URL = "https://api.e2enetworks.com/myaccount/api/v1"
|
||||
|
||||
|
||||
class E2eNetworksSession(BaseModel):
|
||||
"""E2E Networks API session information."""
|
||||
|
||||
api_key: str = Field(exclude=True, repr=False)
|
||||
auth_token: str = Field(exclude=True, repr=False)
|
||||
project_id: int
|
||||
locations: list[str]
|
||||
base_url: str = E2E_BASE_URL
|
||||
http_session: Any = Field(default=None, exclude=True)
|
||||
|
||||
|
||||
class E2eNetworksIdentityInfo(BaseModel):
|
||||
"""E2E Networks identity and scoping information."""
|
||||
|
||||
project_id: int
|
||||
locations: list[str]
|
||||
|
||||
|
||||
class E2eNetworksOutputOptions(ProviderOutputOptions):
|
||||
"""Customize output filenames for E2E Networks scans."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
arguments: object,
|
||||
bulk_checks_metadata: dict,
|
||||
identity: E2eNetworksIdentityInfo,
|
||||
) -> None:
|
||||
"""Initialize E2E Networks output options.
|
||||
|
||||
Args:
|
||||
arguments: Parsed CLI arguments for the scan.
|
||||
bulk_checks_metadata: Loaded metadata for all checks in the scan.
|
||||
identity: E2E Networks identity information used in output filenames.
|
||||
"""
|
||||
super().__init__(arguments, bulk_checks_metadata)
|
||||
if (
|
||||
not hasattr(arguments, "output_filename")
|
||||
or arguments.output_filename is None
|
||||
):
|
||||
self.output_filename = f"prowler-output-e2enetworks-{identity.project_id}-{output_file_timestamp}"
|
||||
else:
|
||||
self.output_filename = arguments.output_filename
|
||||
@@ -0,0 +1,4 @@
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.e2enetworks.services.database.database_service import Database
|
||||
|
||||
database_client = Database(Provider.get_global_provider())
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "database_cluster_backup_enabled",
|
||||
"CheckTitle": "E2E Networks database clusters have automated backups enabled",
|
||||
"CheckType": [],
|
||||
"ServiceName": "database",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "database",
|
||||
"Description": "**E2E Networks database clusters** should have automated backups enabled so data can be restored after failures, corruption, or accidental deletion. Automated backups capture point-in-time copies of the cluster on a recurring schedule.",
|
||||
"Risk": "Database clusters without backups are vulnerable to permanent **data loss** from hardware failures, corruption, or accidental deletion, with no way to recover to a prior state.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/database/rds/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable **automated backups** on every database cluster that does not currently have them configured, and validate that backups can be restored.",
|
||||
"Url": "https://hub.prowler.com/check/database_cluster_backup_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"resilience"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.database.database_client import (
|
||||
database_client,
|
||||
)
|
||||
|
||||
|
||||
class database_cluster_backup_enabled(Check):
|
||||
"""Check if E2E Networks database clusters have backups enabled."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for cluster in database_client.clusters:
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=cluster)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Database cluster {cluster.name} has backups enabled."
|
||||
)
|
||||
if not cluster.backup_enabled:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Database cluster {cluster.name} does not have backups enabled."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "database_cluster_default_admin_username",
|
||||
"CheckTitle": "E2E Networks database clusters do not use the default admin username",
|
||||
"CheckType": [],
|
||||
"ServiceName": "database",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "database",
|
||||
"Description": "**E2E Networks database clusters** should be configured with a custom administrative username instead of the well-known default, reducing the predictability of privileged credentials.",
|
||||
"Risk": "Default admin usernames are publicly known, so they increase exposure to **credential guessing** and **brute-force attacks** against the privileged database account.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/database/rds/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Replace the default administrative username on database clusters with a unique, non-default account name and rotate the associated credentials.",
|
||||
"Url": "https://hub.prowler.com/check/database_cluster_default_admin_username"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.database.database_client import (
|
||||
database_client,
|
||||
)
|
||||
|
||||
|
||||
class database_cluster_default_admin_username(Check):
|
||||
"""Check if E2E Networks database clusters do not use the default admin username."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for cluster in database_client.clusters:
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=cluster)
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Database cluster {cluster.name} does not use the default admin username."
|
||||
if cluster.master_username.lower() == "admin":
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Database cluster {cluster.name} uses the default admin username."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "database_cluster_ip_whitelist_configured",
|
||||
"CheckTitle": "E2E Networks database clusters with public IPs have IP whitelisting configured",
|
||||
"CheckType": [],
|
||||
"ServiceName": "database",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "database",
|
||||
"Description": "**E2E Networks database clusters** that expose a public IP should restrict connectivity through an **IP whitelist** instead of accepting connections from any address (`0.0.0.0/0`).",
|
||||
"Risk": "Publicly exposed database clusters without IP whitelisting accept connections from **any source on the internet**, greatly increasing the risk of unauthorized access and data exfiltration.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/database/rds/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Configure an **IP whitelist** on publicly exposed database clusters to allow only trusted source ranges, or move the cluster onto private networking.",
|
||||
"Url": "https://hub.prowler.com/check/database_cluster_ip_whitelist_configured"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"internet-exposed"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.database.database_client import (
|
||||
database_client,
|
||||
)
|
||||
|
||||
|
||||
class database_cluster_ip_whitelist_configured(Check):
|
||||
"""Check if E2E Networks database clusters with public IPs have IP whitelisting configured."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for cluster in database_client.clusters:
|
||||
if not cluster.master_has_public_ip:
|
||||
continue
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=cluster)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Database cluster {cluster.name} has IP whitelisting configured."
|
||||
)
|
||||
if not cluster.whitelisted_ips:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Database cluster {cluster.name} has a public IP but no whitelisted IPs."
|
||||
findings.append(report)
|
||||
return findings
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "database_cluster_public_ip_not_assigned",
|
||||
"CheckTitle": "E2E Networks database cluster master nodes do not expose a public IP",
|
||||
"CheckType": [],
|
||||
"ServiceName": "database",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "database",
|
||||
"Description": "The **master node** of each **E2E Networks database cluster** should be reachable only over private networking and must not have a public IP address assigned.",
|
||||
"Risk": "A **public database endpoint** is directly reachable from the internet, expanding the attack surface and increasing the risk of unauthorized access, brute-force attempts, and data exposure.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/database/rds/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Remove the **public IP** from the master node of database clusters and access them through private networking such as a VPC.",
|
||||
"Url": "https://hub.prowler.com/check/database_cluster_public_ip_not_assigned"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"internet-exposed"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.database.database_client import (
|
||||
database_client,
|
||||
)
|
||||
|
||||
|
||||
class database_cluster_public_ip_not_assigned(Check):
|
||||
"""Check if E2E Networks database clusters do not expose a public IP on the master node."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for cluster in database_client.clusters:
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=cluster)
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Database cluster {cluster.name} master node does not have a public IP."
|
||||
if cluster.master_has_public_ip:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Database cluster {cluster.name} master node has a public IP assigned."
|
||||
findings.append(report)
|
||||
return findings
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "database_cluster_running",
|
||||
"CheckTitle": "E2E Networks database clusters are in RUNNING status",
|
||||
"CheckType": [],
|
||||
"ServiceName": "database",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "low",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "database",
|
||||
"Description": "**E2E Networks database clusters** should be in the `RUNNING` state, confirming they are operational rather than stopped, failed, or stuck in a transitional status.",
|
||||
"Risk": "Database clusters that are not `RUNNING` may indicate outages, failed provisioning, or misconfiguration that impacts **availability** and the ability to recover data.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/database/rds/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Start database clusters that are not in the `RUNNING` state and investigate the root cause of any stopped or failed clusters.",
|
||||
"Url": "https://hub.prowler.com/check/database_cluster_running"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.database.database_client import (
|
||||
database_client,
|
||||
)
|
||||
|
||||
|
||||
class database_cluster_running(Check):
|
||||
"""Check if E2E Networks database clusters are in RUNNING status."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for cluster in database_client.clusters:
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=cluster)
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Database cluster {cluster.name} is running."
|
||||
if cluster.status != "RUNNING":
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Database cluster {cluster.name} is not running (status: {cluster.status})."
|
||||
findings.append(report)
|
||||
return findings
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "database_cluster_ssl_enabled",
|
||||
"CheckTitle": "E2E Networks database clusters have SSL enabled on the master node",
|
||||
"CheckType": [],
|
||||
"ServiceName": "database",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "database",
|
||||
"Description": "**E2E Networks database clusters** should require `SSL/TLS` on the master node so that client connections and credentials are encrypted **in transit**.",
|
||||
"Risk": "Without `SSL/TLS`, database credentials and query data travel in **plaintext** and can be intercepted through man-in-the-middle or network eavesdropping attacks.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/database/rds/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable `SSL/TLS` on the master node of database clusters to encrypt all client connections in transit.",
|
||||
"Url": "https://hub.prowler.com/check/database_cluster_ssl_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"encryption"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.database.database_client import (
|
||||
database_client,
|
||||
)
|
||||
|
||||
|
||||
class database_cluster_ssl_enabled(Check):
|
||||
"""Check if E2E Networks database clusters have SSL enabled on the master node."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for cluster in database_client.clusters:
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=cluster)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Database cluster {cluster.name} has SSL enabled on the master node."
|
||||
)
|
||||
if not cluster.master_ssl_enabled:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Database cluster {cluster.name} does not have SSL enabled on the master node."
|
||||
findings.append(report)
|
||||
return findings
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "database_replica_public_ip_not_assigned",
|
||||
"CheckTitle": "E2E Networks database read replicas do not have a public IP assigned",
|
||||
"CheckType": [],
|
||||
"ServiceName": "database",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "database",
|
||||
"Description": "**E2E Networks database read replicas** should be reachable only over private networking and must not have a public IP address assigned.",
|
||||
"Risk": "Read replicas with **public IPs** are directly reachable from the internet, expanding database exposure and increasing the risk of unauthorized access to replicated data.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/database/rds/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Remove the **public IP** from database read replicas and route access through private networking.",
|
||||
"Url": "https://hub.prowler.com/check/database_replica_public_ip_not_assigned"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"internet-exposed"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.database.database_client import (
|
||||
database_client,
|
||||
)
|
||||
|
||||
|
||||
class database_replica_public_ip_not_assigned(Check):
|
||||
"""Check if E2E Networks database read replicas do not have a public IP assigned."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for instance in database_client.instances:
|
||||
if instance.role != "replica":
|
||||
continue
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=instance)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Database replica {instance.name} does not have a public IP assigned."
|
||||
)
|
||||
if instance.has_public_ip:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Database replica {instance.name} has a public IP assigned."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -0,0 +1,164 @@
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.e2enetworks.lib.service.service import E2eNetworksService
|
||||
|
||||
|
||||
def _has_public_ip(public_ip_address: str | None) -> bool:
|
||||
if not public_ip_address:
|
||||
return False
|
||||
value = str(public_ip_address).strip()
|
||||
if not value or value.lower() in ("[]", "null", "none"):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class Database(E2eNetworksService):
|
||||
"""Service class for E2E Networks DBaaS (RDS) resources."""
|
||||
|
||||
def __init__(self, provider):
|
||||
super().__init__("database", provider)
|
||||
self.clusters: list[DatabaseCluster] = []
|
||||
self.instances: list[DatabaseInstance] = []
|
||||
self._fetch_clusters()
|
||||
|
||||
def _fetch_clusters(self):
|
||||
for location in self.provider.session.locations:
|
||||
try:
|
||||
cluster_list = self.client.get_data("/rds/cluster/", location=location)
|
||||
if not isinstance(cluster_list, list):
|
||||
continue
|
||||
|
||||
for item in cluster_list:
|
||||
cluster_id = str(item.get("id", ""))
|
||||
detail = self._get_cluster_detail(cluster_id, location)
|
||||
merged = {**item, **detail}
|
||||
|
||||
master_node = merged.get("master_node", {}) or {}
|
||||
database_info = master_node.get("database", {}) or {}
|
||||
software = (
|
||||
merged.get("software", {})
|
||||
or master_node.get("plan", {}).get("software", {})
|
||||
or {}
|
||||
)
|
||||
|
||||
cluster = DatabaseCluster(
|
||||
id=cluster_id,
|
||||
name=merged.get("name", ""),
|
||||
location=location,
|
||||
status=merged.get("status", ""),
|
||||
software_name=software.get("name", ""),
|
||||
software_version=software.get("version", ""),
|
||||
backup_enabled=bool(merged.get("backup_enabled", False)),
|
||||
whitelisted_ips=merged.get("whitelisted_ips", []) or [],
|
||||
master_ssl_enabled=bool(master_node.get("ssl", False)),
|
||||
master_public_ip=master_node.get("public_ip_address"),
|
||||
master_username=database_info.get("username", ""),
|
||||
master_has_public_ip=_has_public_ip(
|
||||
master_node.get("public_ip_address")
|
||||
),
|
||||
)
|
||||
self.clusters.append(cluster)
|
||||
|
||||
self.instances.append(
|
||||
DatabaseInstance(
|
||||
id=str(master_node.get("instance_id", cluster_id)),
|
||||
name=master_node.get("node_name", merged.get("name", "")),
|
||||
cluster_id=cluster_id,
|
||||
cluster_name=cluster.name,
|
||||
location=location,
|
||||
role="master",
|
||||
public_ip_address=master_node.get("public_ip_address"),
|
||||
has_public_ip=_has_public_ip(
|
||||
master_node.get("public_ip_address")
|
||||
),
|
||||
ssl_enabled=bool(master_node.get("ssl", False)),
|
||||
username=database_info.get("username", ""),
|
||||
)
|
||||
)
|
||||
|
||||
for slave in merged.get("slave_nodes", []) or []:
|
||||
if not isinstance(slave, dict):
|
||||
continue
|
||||
slave_db = slave.get("database", {}) or {}
|
||||
self.instances.append(
|
||||
DatabaseInstance(
|
||||
id=str(slave.get("instance_id", "")),
|
||||
name=slave.get("node_name", ""),
|
||||
cluster_id=cluster_id,
|
||||
cluster_name=cluster.name,
|
||||
location=location,
|
||||
role="replica",
|
||||
public_ip_address=slave.get("public_ip_address"),
|
||||
has_public_ip=_has_public_ip(
|
||||
slave.get("public_ip_address")
|
||||
),
|
||||
ssl_enabled=bool(slave.get("ssl", False)),
|
||||
username=slave_db.get("username", ""),
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"database - Error fetching clusters in {location} -- "
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_cluster_detail(self, cluster_id: str, location: str) -> dict:
|
||||
if not cluster_id:
|
||||
return {}
|
||||
try:
|
||||
data = self.client.get_data(
|
||||
f"/rds/cluster/{cluster_id}/",
|
||||
location=location,
|
||||
)
|
||||
return data if isinstance(data, dict) else {}
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"database - Error fetching cluster detail {cluster_id} -- "
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return {}
|
||||
|
||||
|
||||
class DatabaseCluster(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
location: str
|
||||
status: str = ""
|
||||
software_name: str = ""
|
||||
software_version: str = ""
|
||||
backup_enabled: bool = False
|
||||
whitelisted_ips: list = []
|
||||
master_ssl_enabled: bool = False
|
||||
master_public_ip: str | None = None
|
||||
master_username: str = ""
|
||||
master_has_public_ip: bool = False
|
||||
|
||||
@property
|
||||
def resource_id(self) -> str:
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def resource_name(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class DatabaseInstance(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
cluster_id: str
|
||||
cluster_name: str
|
||||
location: str
|
||||
role: str
|
||||
public_ip_address: str | None = None
|
||||
has_public_ip: bool = False
|
||||
ssl_enabled: bool = False
|
||||
username: str = ""
|
||||
|
||||
@property
|
||||
def resource_id(self) -> str:
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def resource_name(self) -> str:
|
||||
return self.name
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "loadbalancer_alb_https_uses_ssl_certificate",
|
||||
"CheckTitle": "E2E Networks ALB HTTPS load balancers use an SSL certificate",
|
||||
"CheckType": [],
|
||||
"ServiceName": "loadbalancer",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "network",
|
||||
"Description": "**E2E Networks application load balancers** serving `HTTPS` traffic should have a valid **SSL certificate** configured so client connections are encrypted and authenticated.",
|
||||
"Risk": "`HTTPS` load balancers without a valid **SSL certificate** expose traffic to interception and downgrade attacks, weakening **confidentiality** and **integrity** for client connections.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/compute/load-balancer/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Attach a valid **SSL certificate** to every load balancer that serves `HTTPS` traffic.",
|
||||
"Url": "https://hub.prowler.com/check/loadbalancer_alb_https_uses_ssl_certificate"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"encryption"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_client import (
|
||||
loadbalancer_client,
|
||||
)
|
||||
|
||||
|
||||
class loadbalancer_alb_https_uses_ssl_certificate(Check):
|
||||
"""Check that HTTPS load balancers have an SSL certificate configured."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for lb in loadbalancer_client.load_balancers:
|
||||
if not lb.is_alb_https:
|
||||
continue
|
||||
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=lb)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Load balancer {lb.name} uses an SSL certificate for HTTPS traffic."
|
||||
)
|
||||
if not lb.ssl_certificate_id:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Load balancer {lb.name} does not have an SSL certificate configured for HTTPS traffic."
|
||||
findings.append(report)
|
||||
return findings
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "loadbalancer_backend_health_check_enabled",
|
||||
"CheckTitle": "E2E Networks ALB load balancers have backend health checks enabled",
|
||||
"CheckType": [],
|
||||
"ServiceName": "loadbalancer",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "network",
|
||||
"Description": "**E2E Networks application load balancers** should have `HTTP` **health checks** configured for their backends so traffic is only routed to healthy nodes.",
|
||||
"Risk": "Load balancers without backend **health checks** may route traffic to failed or unhealthy nodes, causing outages and masking active compromise on degraded backends.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/compute/load-balancer/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Configure `HTTP` **health checks** for ALB backends so unhealthy nodes are automatically removed from rotation.",
|
||||
"Url": "https://hub.prowler.com/check/loadbalancer_backend_health_check_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"resilience"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_client import (
|
||||
loadbalancer_client,
|
||||
)
|
||||
|
||||
|
||||
class loadbalancer_backend_health_check_enabled(Check):
|
||||
"""Check that ALB load balancers have backend health checks configured."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for lb in loadbalancer_client.load_balancers:
|
||||
if not lb.is_alb:
|
||||
continue
|
||||
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=lb)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Load balancer {lb.name} has backend health checks configured."
|
||||
)
|
||||
if not lb.has_backend_health_check:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Load balancer {lb.name} does not have backend health checks configured."
|
||||
findings.append(report)
|
||||
return findings
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "loadbalancer_bitninja_enabled",
|
||||
"CheckTitle": "E2E Networks load balancers have BitNinja protection enabled",
|
||||
"CheckType": [],
|
||||
"ServiceName": "loadbalancer",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "low",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "network",
|
||||
"Description": "**E2E Networks load balancers** should have **BitNinja** protection enabled to provide web application firewall coverage on public endpoints.",
|
||||
"Risk": "Load balancers without **BitNinja** protection have reduced **WAF** coverage, increasing exposure to automated attacks and malicious traffic against public endpoints.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/compute/load-balancer/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable **BitNinja** protection on load balancer appliances to strengthen web application firewall coverage.",
|
||||
"Url": "https://hub.prowler.com/check/loadbalancer_bitninja_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_client import (
|
||||
loadbalancer_client,
|
||||
)
|
||||
|
||||
|
||||
class loadbalancer_bitninja_enabled(Check):
|
||||
"""Check that load balancers have BitNinja protection enabled."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for lb in loadbalancer_client.load_balancers:
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=lb)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Load balancer {lb.name} has BitNinja protection enabled."
|
||||
)
|
||||
if not lb.enable_bitninja:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Load balancer {lb.name} does not have BitNinja protection enabled."
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -0,0 +1,6 @@
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_service import (
|
||||
LoadBalancers,
|
||||
)
|
||||
|
||||
loadbalancer_client = LoadBalancers(Provider.get_global_provider())
|
||||
@@ -0,0 +1,96 @@
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.e2enetworks.lib.service.service import E2eNetworksService
|
||||
|
||||
|
||||
class LoadBalancers(E2eNetworksService):
|
||||
"""Service class for E2E Networks load balancers."""
|
||||
|
||||
def __init__(self, provider):
|
||||
super().__init__("loadbalancer", provider)
|
||||
self.load_balancers: list[LoadBalancer] = []
|
||||
self._fetch_loadbalancers()
|
||||
|
||||
def _fetch_loadbalancers(self):
|
||||
for location in self.provider.session.locations:
|
||||
try:
|
||||
appliances = self.client.paginate(
|
||||
"/appliances/",
|
||||
location=location,
|
||||
)
|
||||
for item in appliances:
|
||||
context = self._extract_context(item)
|
||||
node_detail = item.get("node_detail", {}) or {}
|
||||
self.load_balancers.append(
|
||||
LoadBalancer(
|
||||
id=str(item.get("id", "")),
|
||||
name=item.get("name", ""),
|
||||
location=location,
|
||||
status=item.get("status", ""),
|
||||
lb_mode=context.get("lb_mode", ""),
|
||||
lb_port=str(context.get("lb_port", "")),
|
||||
enable_bitninja=bool(context.get("enable_bitninja", False)),
|
||||
ssl_certificate_id=self._get_ssl_certificate_id(context),
|
||||
backends=context.get("backends", []) or [],
|
||||
public_ip=node_detail.get("public_ip", ""),
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"loadbalancer - Error fetching appliances in {location} -- "
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _extract_context(item: dict) -> dict:
|
||||
instances = item.get("appliance_instance", []) or []
|
||||
if not instances:
|
||||
return {}
|
||||
return instances[0].get("context", {}) or {}
|
||||
|
||||
@staticmethod
|
||||
def _get_ssl_certificate_id(context: dict) -> str | None:
|
||||
ssl_context = context.get("ssl_context", {}) or {}
|
||||
certificate_id = ssl_context.get("ssl_certificate_id")
|
||||
if certificate_id in (None, "", 0):
|
||||
return None
|
||||
return str(certificate_id)
|
||||
|
||||
|
||||
class LoadBalancer(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
location: str
|
||||
status: str = ""
|
||||
lb_mode: str = ""
|
||||
lb_port: str = ""
|
||||
enable_bitninja: bool = False
|
||||
ssl_certificate_id: str | None = None
|
||||
backends: list = []
|
||||
public_ip: str = ""
|
||||
|
||||
@property
|
||||
def resource_id(self) -> str:
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def resource_name(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def is_alb(self) -> bool:
|
||||
mode = self.lb_mode.upper()
|
||||
return mode in ("HTTP", "HTTPS", "BOTH")
|
||||
|
||||
@property
|
||||
def is_alb_https(self) -> bool:
|
||||
mode = self.lb_mode.upper()
|
||||
return mode in ("HTTPS", "BOTH")
|
||||
|
||||
@property
|
||||
def has_backend_health_check(self) -> bool:
|
||||
for backend in self.backends:
|
||||
if isinstance(backend, dict) and backend.get("http_check"):
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,4 @@
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.e2enetworks.services.network.network_service import Network
|
||||
|
||||
network_client = Network(Provider.get_global_provider())
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "network_reserveip_floating_ip_unattached",
|
||||
"CheckTitle": "E2E Networks floating IPs are attached to nodes",
|
||||
"CheckType": [],
|
||||
"ServiceName": "network",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "network",
|
||||
"Description": "**E2E Networks floating (reserved) IP addresses** should be attached to a node instead of being left unassigned, since idle floating IPs can be reassigned unexpectedly.",
|
||||
"Risk": "**Unattached floating IPs** may be reassigned unexpectedly to other resources, disrupting secure routing assumptions and potentially exposing services.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/network/reserve-ip/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Attach unassigned **floating IPs** to the intended node, or release them if they are no longer needed.",
|
||||
"Url": "https://hub.prowler.com/check/network_reserveip_floating_ip_unattached"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.network.network_client import network_client
|
||||
|
||||
|
||||
class network_reserveip_floating_ip_unattached(Check):
|
||||
"""Check if E2E Networks floating IPs are attached to nodes."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for ip in network_client.reserved_ips:
|
||||
if ip.reserved_type != "FloatingIP":
|
||||
continue
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=ip)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Floating IP {ip.ip_address} is attached to node(s)."
|
||||
)
|
||||
if ip.status != "Attached" or ip.floating_ip_attached_nodes_count == 0:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Floating IP {ip.ip_address} is not attached to any node."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "network_reserveip_orphaned_public_ip",
|
||||
"CheckTitle": "E2E Networks public or addon IPs are attached to a resource",
|
||||
"CheckType": [],
|
||||
"ServiceName": "network",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "network",
|
||||
"Description": "**E2E Networks public and addon reserved IP addresses** should be attached to a resource, identifying orphaned addresses that remain internet-reachable without an owner.",
|
||||
"Risk": "**Orphaned public IPs** remain internet-reachable resources that can be misused or reassigned without ownership controls, expanding the attack surface.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/network/reserve-ip/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Attach orphaned **public or addon IPs** to a resource, or delete them to remove unused internet-reachable addresses.",
|
||||
"Url": "https://hub.prowler.com/check/network_reserveip_orphaned_public_ip"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"internet-exposed"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.network.network_client import network_client
|
||||
|
||||
|
||||
class network_reserveip_orphaned_public_ip(Check):
|
||||
"""Check if E2E Networks public or addon IPs are attached."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for ip in network_client.reserved_ips:
|
||||
if ip.reserved_type not in ("PublicIP", "AddonIP"):
|
||||
continue
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=ip)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Reserved IP {ip.ip_address} is attached to a resource."
|
||||
)
|
||||
if ip.status != "Attached" or ip.vm_id is None:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Reserved IP {ip.ip_address} is orphaned (status: {ip.status})."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -0,0 +1,158 @@
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.e2enetworks.lib.service.service import E2eNetworksService
|
||||
|
||||
|
||||
class Network(E2eNetworksService):
|
||||
"""Service class for E2E Networks network resources."""
|
||||
|
||||
def __init__(self, provider):
|
||||
super().__init__("network", provider)
|
||||
self.vpcs: list[Vpc] = []
|
||||
self.reserved_ips: list[ReservedIp] = []
|
||||
self.vpc_tunnels: list[VpcTunnel] = []
|
||||
self._fetch_vpcs()
|
||||
self._fetch_reserved_ips()
|
||||
self._fetch_vpc_tunnels()
|
||||
|
||||
def _fetch_vpcs(self):
|
||||
for location in self.provider.session.locations:
|
||||
try:
|
||||
vpcs = self.client.paginate("/vpc/list/", location=location)
|
||||
if not isinstance(vpcs, list):
|
||||
continue
|
||||
for item in vpcs:
|
||||
gateway_node = item.get("gateway_node", {}) or {}
|
||||
self.vpcs.append(
|
||||
Vpc(
|
||||
network_id=str(item.get("network_id", "")),
|
||||
name=item.get("name", ""),
|
||||
location=location,
|
||||
is_active=bool(item.get("is_active", False)),
|
||||
state=item.get("state", ""),
|
||||
ipv4_cidr=item.get("ipv4_cidr", ""),
|
||||
vm_count=int(item.get("vm_count", 0)),
|
||||
gateway_node_id=str(gateway_node.get("node_id", "")),
|
||||
gateway_public_ip=gateway_node.get("ip_address_public", ""),
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"network - Error fetching VPCs in {location} -- "
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _fetch_reserved_ips(self):
|
||||
for location in self.provider.session.locations:
|
||||
try:
|
||||
ips = self.client.get_data("/reserve_ips/", location=location)
|
||||
if not isinstance(ips, list):
|
||||
continue
|
||||
|
||||
for item in ips:
|
||||
attached_nodes = item.get("floating_ip_attached_nodes", []) or []
|
||||
self.reserved_ips.append(
|
||||
ReservedIp(
|
||||
reserve_id=str(item.get("reserve_id", "")),
|
||||
ip_address=item.get("ip_address", ""),
|
||||
location=location,
|
||||
status=item.get("status", ""),
|
||||
reserved_type=item.get("reserved_type", ""),
|
||||
vm_id=item.get("vm_id"),
|
||||
floating_ip_attached_nodes_count=len(attached_nodes),
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"network - Error fetching reserved IPs in {location} -- "
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _fetch_vpc_tunnels(self):
|
||||
for vpc in self.vpcs:
|
||||
if not vpc.network_id:
|
||||
continue
|
||||
try:
|
||||
tunnels = self.client.get_data(
|
||||
f"/vpc/tunnels/{vpc.network_id}/",
|
||||
location=vpc.location,
|
||||
)
|
||||
if not isinstance(tunnels, list):
|
||||
continue
|
||||
|
||||
for item in tunnels:
|
||||
self.vpc_tunnels.append(
|
||||
VpcTunnel(
|
||||
id=str(item.get("id", "")),
|
||||
name=item.get("name", ""),
|
||||
location=vpc.location,
|
||||
local_vpc_network_id=vpc.network_id,
|
||||
local_vpc_name=vpc.name,
|
||||
status=item.get("status", ""),
|
||||
is_peer_vpc_external=bool(
|
||||
item.get("is_peer_vpc_external", False)
|
||||
),
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"network - Error fetching tunnels for VPC {vpc.network_id} -- "
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
class Vpc(BaseModel):
|
||||
network_id: str
|
||||
name: str
|
||||
location: str
|
||||
is_active: bool = False
|
||||
state: str = ""
|
||||
ipv4_cidr: str = ""
|
||||
vm_count: int = 0
|
||||
gateway_node_id: str = ""
|
||||
gateway_public_ip: str = ""
|
||||
|
||||
@property
|
||||
def resource_id(self) -> str:
|
||||
return self.network_id
|
||||
|
||||
@property
|
||||
def resource_name(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class ReservedIp(BaseModel):
|
||||
reserve_id: str
|
||||
ip_address: str
|
||||
location: str
|
||||
status: str = ""
|
||||
reserved_type: str = ""
|
||||
vm_id: int | None = None
|
||||
floating_ip_attached_nodes_count: int = 0
|
||||
|
||||
@property
|
||||
def resource_id(self) -> str:
|
||||
return self.reserve_id
|
||||
|
||||
@property
|
||||
def resource_name(self) -> str:
|
||||
return self.ip_address
|
||||
|
||||
|
||||
class VpcTunnel(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
location: str
|
||||
local_vpc_network_id: str
|
||||
local_vpc_name: str
|
||||
status: str = ""
|
||||
is_peer_vpc_external: bool = False
|
||||
|
||||
@property
|
||||
def resource_id(self) -> str:
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def resource_name(self) -> str:
|
||||
return self.name
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "network_vpc_has_attached_nodes",
|
||||
"CheckTitle": "E2E Networks VPCs have attached nodes",
|
||||
"CheckType": [],
|
||||
"ServiceName": "network",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "low",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "network",
|
||||
"Description": "**E2E Networks VPCs** should have at least one node attached, identifying unused network segments that may indicate misconfiguration or forgotten resources.",
|
||||
"Risk": "**VPCs without attached nodes** may indicate unused network segments or misconfiguration that weakens **network segmentation** and increases management overhead.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/network/vpc/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Attach nodes to VPCs that are in use, or remove empty VPCs that are no longer required.",
|
||||
"Url": "https://hub.prowler.com/check/network_vpc_has_attached_nodes"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.network.network_client import network_client
|
||||
|
||||
|
||||
class network_vpc_has_attached_nodes(Check):
|
||||
"""Check if E2E Networks VPCs have attached nodes."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for vpc in network_client.vpcs:
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=vpc)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"VPC {vpc.name} has {vpc.vm_count} attached node(s)."
|
||||
)
|
||||
if vpc.vm_count <= 0:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"VPC {vpc.name} has no attached nodes."
|
||||
findings.append(report)
|
||||
return findings
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "network_vpc_is_active",
|
||||
"CheckTitle": "E2E Networks VPCs are active",
|
||||
"CheckType": [],
|
||||
"ServiceName": "network",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "network",
|
||||
"Description": "**E2E Networks VPCs** should be in the `active` state so that attached workloads retain expected connectivity and network isolation.",
|
||||
"Risk": "**Inactive VPCs** can cause connectivity failures and may leave workloads without the expected **network isolation** boundaries.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/network/vpc/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Activate VPCs that are `inactive` so dependent workloads keep their expected connectivity and isolation.",
|
||||
"Url": "https://hub.prowler.com/check/network_vpc_is_active"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.network.network_client import network_client
|
||||
|
||||
|
||||
class network_vpc_is_active(Check):
|
||||
"""Check if E2E Networks VPCs are active."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for vpc in network_client.vpcs:
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=vpc)
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"VPC {vpc.name} is active."
|
||||
if not vpc.is_active or vpc.state != "Active":
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"VPC {vpc.name} is not active (state: {vpc.state})."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "network_vpc_peering_external_peer_disabled",
|
||||
"CheckTitle": "E2E Networks VPC peering does not use external peers",
|
||||
"CheckType": [],
|
||||
"ServiceName": "network",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "network",
|
||||
"Description": "**E2E Networks VPC peering connections** should be established only with internal networks and must not extend trust to external peers outside the account.",
|
||||
"Risk": "**VPC peering with external peers** expands trust boundaries and can expose private networks to third parties, enabling lateral movement across accounts.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/network/vpc/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Remove **VPC peering** tunnels that connect to external peers to keep private networks within trusted boundaries.",
|
||||
"Url": "https://hub.prowler.com/check/network_vpc_peering_external_peer_disabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"trust-boundaries"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.network.network_client import network_client
|
||||
|
||||
|
||||
class network_vpc_peering_external_peer_disabled(Check):
|
||||
"""Check if E2E Networks VPC peering does not use external peers."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for tunnel in network_client.vpc_tunnels:
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=tunnel)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"VPC peering {tunnel.name} does not use an external peer VPC."
|
||||
)
|
||||
if tunnel.is_peer_vpc_external:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"VPC peering {tunnel.name} uses an external peer VPC."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "node_accidental_protection_enabled",
|
||||
"CheckTitle": "E2E Networks nodes have accidental protection enabled",
|
||||
"CheckType": [],
|
||||
"ServiceName": "node",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "compute",
|
||||
"Description": "**E2E Networks nodes** should have **accidental protection** enabled to guard against unintended deletion or modification of compute instances.",
|
||||
"Risk": "Nodes without **accidental protection** are easier to delete or modify unintentionally, increasing the risk of **service disruption** and unplanned exposure of compute resources.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/compute/nodes/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable **accidental protection** on nodes to prevent unintended deletion or modification.",
|
||||
"Url": "https://hub.prowler.com/check/node_accidental_protection_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"resilience"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.node.node_client import node_client
|
||||
|
||||
|
||||
class node_accidental_protection_enabled(Check):
|
||||
"""Check if E2E Networks nodes have accidental protection enabled."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for node in node_client.nodes:
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=node)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Node {node.name} has accidental protection enabled."
|
||||
)
|
||||
if not node.is_accidental_protection:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Node {node.name} does not have accidental protection enabled."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -0,0 +1,4 @@
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.e2enetworks.services.node.node_service import Nodes
|
||||
|
||||
node_client = Nodes(Provider.get_global_provider())
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "node_compliance_enabled",
|
||||
"CheckTitle": "E2E Networks nodes have compliance mode enabled",
|
||||
"CheckType": [],
|
||||
"ServiceName": "node",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "compute",
|
||||
"Description": "**E2E Networks nodes** should run with **compliance mode** enabled so required security controls are enforced and workload activity remains auditable.",
|
||||
"Risk": "Nodes without **compliance mode** may not enforce required security controls, increasing misconfiguration risk and weakening the **auditability** of compute workloads.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/compute/nodes/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable **compliance mode** on nodes to enforce required security controls and maintain auditability.",
|
||||
"Url": "https://hub.prowler.com/check/node_compliance_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
from prowler.lib.check.models import Check, CheckReportE2eNetworks
|
||||
from prowler.providers.e2enetworks.services.node.node_client import node_client
|
||||
|
||||
|
||||
class node_compliance_enabled(Check):
|
||||
"""Check if E2E Networks nodes have compliance mode enabled."""
|
||||
|
||||
def execute(self) -> list[CheckReportE2eNetworks]:
|
||||
findings = []
|
||||
for node in node_client.nodes:
|
||||
report = CheckReportE2eNetworks(metadata=self.metadata(), resource=node)
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Node {node.name} has compliance mode enabled."
|
||||
if not node.is_node_compliance:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Node {node.name} does not have compliance mode enabled."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Provider": "e2enetworks",
|
||||
"CheckID": "node_encryption_enabled",
|
||||
"CheckTitle": "E2E Networks nodes have disk encryption enabled",
|
||||
"CheckType": [],
|
||||
"ServiceName": "node",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "compute",
|
||||
"Description": "**E2E Networks nodes** should have **disk encryption** enabled so data stored on node volumes and snapshots is protected **at rest**.",
|
||||
"Risk": "**Unencrypted nodes** increase the risk of **data exposure** if disks or snapshots are accessed outside normal operating controls.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.e2enetworks.com/api/myaccount/compute/nodes/"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable **disk encryption** on nodes to protect data at rest on volumes and snapshots.",
|
||||
"Url": "https://hub.prowler.com/check/node_encryption_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"encryption"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user