Compare commits

...
Author SHA1 Message Date
Toni de la FuenteandClaude Fable 5 93935b2cff feat(oracledb): add provider to the SDK with 20 security checks
- Add python-oracledb thin-mode provider with user/password authentication
- Add users, privileges, audit, encryption and configuration services
- Add 20 checks based on the Oracle Database Security Assessment Tool (DBSAT)
- Register the provider across CLI, outputs, mutelist, labeler and CI
- Add unit tests for the provider, services and every check

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 16:26:09 +02:00
136 changed files with 4967 additions and 4 deletions
+7
View File
@@ -82,6 +82,11 @@ provider/linode:
- any-glob-to-any-file: "prowler/providers/linode/**"
- any-glob-to-any-file: "tests/providers/linode/**"
provider/oracledb:
- changed-files:
- any-glob-to-any-file: "prowler/providers/oracledb/**"
- any-glob-to-any-file: "tests/providers/oracledb/**"
github_actions:
- changed-files:
- any-glob-to-any-file: ".github/workflows/*"
@@ -121,6 +126,8 @@ mutelist:
- any-glob-to-any-file: "tests/providers/vercel/lib/mutelist/**"
- any-glob-to-any-file: "prowler/providers/okta/lib/mutelist/**"
- any-glob-to-any-file: "tests/providers/okta/lib/mutelist/**"
- any-glob-to-any-file: "prowler/providers/oracledb/lib/mutelist/**"
- any-glob-to-any-file: "tests/providers/oracledb/lib/mutelist/**"
integration/s3:
- changed-files:
+24
View File
@@ -351,6 +351,30 @@ jobs:
flags: prowler-py${{ matrix.python-version }}-okta
files: ./okta_coverage.xml
# Oracle Database Provider
- name: Check if Oracle Database files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-oracledb
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
./prowler/**/oracledb/**
./tests/**/oracledb/**
./uv.lock
- name: Run Oracle Database tests
if: steps.changed-oracledb.outputs.any_changed == 'true'
run: uv run pytest -n auto --cov=./prowler/providers/oracledb --cov-report=xml:oracledb_coverage.xml tests/providers/oracledb
- name: Upload Oracle Database coverage to Codecov
if: steps.changed-oracledb.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 }}-oracledb
files: ./oracledb_coverage.xml
# NHN Provider
- name: Check if NHN files changed
if: steps.check-changes.outputs.any_changed == 'true'
+5
View File
@@ -156,6 +156,7 @@ from prowler.providers.nhn.models import NHNOutputOptions
from prowler.providers.okta.models import OktaOutputOptions
from prowler.providers.openstack.models import OpenStackOutputOptions
from prowler.providers.oraclecloud.models import OCIOutputOptions
from prowler.providers.oracledb.models import OracledbOutputOptions
from prowler.providers.scaleway.models import ScalewayOutputOptions
from prowler.providers.stackit.models import StackITOutputOptions
from prowler.providers.vercel.models import VercelOutputOptions
@@ -441,6 +442,10 @@ def prowler():
output_options = OktaOutputOptions(
args, bulk_checks_metadata, global_provider.identity
)
elif provider == "oracledb":
output_options = OracledbOutputOptions(
args, bulk_checks_metadata, global_provider.identity
)
elif provider == "scaleway":
output_options = ScalewayOutputOptions(
args, bulk_checks_metadata, global_provider.identity
@@ -0,0 +1 @@
Oracle Database provider (`oracledb`) with 20 security checks based on the Oracle Database Security Assessment Tool (DBSAT) covering users, privileges, auditing, encryption and configuration
+1
View File
@@ -79,6 +79,7 @@ class Provider(str, Enum):
SCALEWAY = "scaleway"
VERCEL = "vercel"
OKTA = "okta"
ORACLEDB = "oracledb"
STACKIT = "stackit"
LINODE = "linode"
E2ENETWORKS = "e2enetworks"
@@ -0,0 +1,20 @@
### Account, Check and/or Region can be * to apply for all the cases.
### Account == <Oracle Database global name, e.g. ORCL.EXAMPLE.COM>
### The value of the GLOBAL_NAME view, as shown by prowler oracledb.
### Region is always "*" — a database has no regional concept.
### Resources matches against the resource name (e.g. a username, a
### tablespace name or an initialization parameter name).
### Resources and tags are lists that can have either Regex or Keywords.
### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together.
### Use an alternation Regex to match one of multiple tags with "ORed" logic.
### For each check you can except Accounts, Regions, Resources and/or Tags.
########################### MUTELIST EXAMPLE ###########################
Mutelist:
Accounts:
"ORCL.EXAMPLE.COM":
Checks:
"users_sample_schemas_removed":
Regions:
- "*"
Resources:
- "ORCL.EXAMPLE.COM"
+4
View File
@@ -797,6 +797,10 @@ def execute(
is_finding_muted_args["org_domain"] = (
global_provider.identity.org_domain
)
elif global_provider.type == "oracledb":
is_finding_muted_args["database_name"] = (
global_provider.identity.database_name
)
elif global_provider.type == "linode":
is_finding_muted_args["account_id"] = (
global_provider.identity.account_id
+35
View File
@@ -993,6 +993,41 @@ class CheckReportOkta(Check_Report):
self.region = region
@dataclass
class CheckReportOracledb(Check_Report):
"""Contains the Oracle Database Check's finding information."""
resource_name: str
resource_id: str
database_name: str
region: str
def __init__(
self,
metadata: Dict,
resource: Any,
resource_name: str = None,
resource_id: str = None,
database_name: str = None,
region: str = "global",
) -> None:
"""Initialize the Oracle Database Check's finding information.
Args:
metadata: The metadata of the check.
resource: Basic information about the resource.
resource_name: The name of the resource related with the finding.
resource_id: The id of the resource related with the finding.
database_name: The database global name related with the finding.
region: Always "global" — a database has no regional concept.
"""
super().__init__(metadata, resource)
self.resource_name = resource_name or getattr(resource, "name", "")
self.resource_id = resource_id or getattr(resource, "id", "")
self.database_name = database_name or getattr(resource, "database_name", "")
self.region = region
@dataclass
class CheckReportGoogleWorkspace(Check_Report):
"""Contains the Google Workspace Check's finding information."""
+4 -2
View File
@@ -50,6 +50,7 @@ class ProwlerArgumentParser:
"vercel",
"e2enetworks",
"okta",
"oracledb",
"scaleway",
"stackit",
"linode",
@@ -75,10 +76,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,e2enetworks,dashboard,iac,image,llm{extra_providers_csv}}} ...",
usage=f"prowler [-h] [--version] {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,nhn,mongodbatlas,oraclecloud,oracledb,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,e2enetworks{extra_providers_csv}}}
{{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,iac,llm,image,nhn,mongodbatlas,oraclecloud,oracledb,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,linode,e2enetworks{extra_providers_csv}}}
aws AWS Provider
azure Azure Provider
gcp GCP Provider
@@ -89,6 +90,7 @@ Available Cloud Providers:
okta Okta Provider
cloudflare Cloudflare Provider
oraclecloud Oracle Cloud Infrastructure Provider
oracledb Oracle Database Provider
openstack OpenStack Provider
stackit StackIT Provider
alibabacloud Alibaba Cloud Provider
+12
View File
@@ -468,6 +468,18 @@ class Finding(BaseModel):
output_data["resource_uid"] = check_output.resource_id
output_data["region"] = "global"
elif provider.type == "oracledb":
output_data["auth_method"] = provider.auth_method
output_data["account_uid"] = get_nested_attribute(
provider, "identity.database_name"
)
output_data["account_name"] = get_nested_attribute(
provider, "identity.database_name"
)
output_data["resource_name"] = check_output.resource_name
output_data["resource_uid"] = check_output.resource_id
output_data["region"] = "global"
elif provider.type == "scaleway":
output_data["auth_method"] = "api_key"
output_data["account_uid"] = get_nested_attribute(
+53
View File
@@ -1552,6 +1552,59 @@ class HTML(Output):
)
return ""
@staticmethod
def get_oracledb_assessment_summary(provider: Provider) -> str:
"""
get_oracledb_assessment_summary gets the HTML assessment summary for the Oracle Database provider
Args:
provider (Provider): the Oracle Database provider object
Returns:
str: HTML assessment summary for the Oracle Database provider
"""
try:
assessment_items = f"""
<li class="list-group-item">
<b>Oracle Database:</b> {provider.identity.database_name}
</li>
<li class="list-group-item">
<b>DSN:</b> {provider.identity.dsn}
</li>"""
credentials_items = f"""
<li class="list-group-item">
<b>Authentication:</b> {provider.auth_method}
</li>
<li class="list-group-item">
<b>User:</b> {provider.identity.user}
</li>"""
return f"""
<div class="col-md-2">
<div class="card">
<div class="card-header">
Oracle Database Assessment Summary
</div>
<ul class="list-group list-group-flush">{assessment_items}
</ul>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
Oracle Database Credentials
</div>
<ul class="list-group list-group-flush">{credentials_items}
</ul>
</div>
</div>"""
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
return ""
@staticmethod
def get_scaleway_assessment_summary(provider: Provider) -> str:
"""
+2
View File
@@ -46,6 +46,8 @@ def stdout_report(finding, color, verbose, status, fix, provider=None):
details = finding.region
elif finding.check_metadata.Provider == "okta":
details = finding.region
elif finding.check_metadata.Provider == "oracledb":
details = finding.region
elif finding.check_metadata.Provider == "scaleway":
details = finding.region
elif finding.check_metadata.Provider == "linode":
+3
View File
@@ -121,6 +121,9 @@ def display_summary_table(
elif provider.type == "okta":
entity_type = "Okta Org"
audited_entities = provider.identity.org_domain
elif provider.type == "oracledb":
entity_type = "Oracle Database"
audited_entities = provider.identity.database_name
elif provider.type == "scaleway":
entity_type = "Organization"
audited_entities = provider.identity.organization_id
+14
View File
@@ -654,6 +654,20 @@ class Provider(ABC):
mutelist_path=arguments.mutelist_file,
fixer_config=fixer_config,
)
elif arguments.provider == "oracledb":
provider_class(
oracledb_user=getattr(arguments, "oracledb_user", ""),
oracledb_password=getattr(arguments, "oracledb_password", ""),
oracledb_dsn=getattr(arguments, "oracledb_dsn", ""),
oracledb_host=getattr(arguments, "oracledb_host", ""),
oracledb_port=getattr(arguments, "oracledb_port", None),
oracledb_service_name=getattr(
arguments, "oracledb_service_name", ""
),
config_path=arguments.config_file,
mutelist_path=arguments.mutelist_file,
fixer_config=fixer_config,
)
elif arguments.provider == "scaleway":
# Credentials are read from the SCW_ACCESS_KEY /
# SCW_SECRET_KEY env vars by the provider itself; there
@@ -0,0 +1,112 @@
from prowler.exceptions.exceptions import ProwlerException
# Exceptions codes from 20000 to 20999 are reserved for Oracle Database exceptions
class OracledbBaseException(ProwlerException):
"""Base class for Oracle Database Errors."""
ORACLEDB_ERROR_CODES = {
(20000, "OracledbEnvironmentVariableError"): {
"message": "Oracle Database environment variable error",
"remediation": "Check the Oracle Database environment variables and ensure they are properly set.",
},
(20001, "OracledbSetUpSessionError"): {
"message": "Error setting up Oracle Database session",
"remediation": "Check the connection credentials (user, password, DSN) and ensure the database is reachable.",
},
(20002, "OracledbSetUpIdentityError"): {
"message": "Oracle Database identity setup error due to bad credentials",
"remediation": "Check the connection credentials and confirm the user can query the database dictionary views.",
},
(20003, "OracledbInvalidCredentialsError"): {
"message": "Oracle Database credentials are not valid",
"remediation": "Check the user and password for the Oracle Database connection.",
},
(20004, "OracledbConnectionError"): {
"message": "Could not connect to the Oracle Database",
"remediation": "Check the DSN (host:port/service_name), network connectivity and listener status.",
},
(20005, "OracledbInvalidProviderIdError"): {
"message": "The provided provider_id does not match the connected database",
"remediation": "Check the provider_id (Oracle Database global name) and ensure it matches the database the credentials connect to.",
},
(20006, "OracledbInsufficientPrivilegesError"): {
"message": "Oracle Database user is missing required privileges",
"remediation": "Grant the SELECT ANY DICTIONARY system privilege (or the SELECT_CATALOG_ROLE role) to the assessment user.",
},
}
def __init__(self, code, file=None, original_exception=None, message=None):
provider = "OracleDB"
error_info = self.ORACLEDB_ERROR_CODES.get((code, self.__class__.__name__))
if error_info is None:
error_info = {
"message": message or "Unknown Oracle Database error.",
"remediation": "Check the Oracle Database 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 OracledbCredentialsError(OracledbBaseException):
"""Base class for Oracle Database credentials errors."""
def __init__(self, code, file=None, original_exception=None, message=None):
super().__init__(code, file, original_exception, message)
class OracledbEnvironmentVariableError(OracledbCredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
20000, file=file, original_exception=original_exception, message=message
)
class OracledbSetUpSessionError(OracledbCredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
20001, file=file, original_exception=original_exception, message=message
)
class OracledbSetUpIdentityError(OracledbCredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
20002, file=file, original_exception=original_exception, message=message
)
class OracledbInvalidCredentialsError(OracledbCredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
20003, file=file, original_exception=original_exception, message=message
)
class OracledbConnectionError(OracledbCredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
20004, file=file, original_exception=original_exception, message=message
)
class OracledbInvalidProviderIdError(OracledbCredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
20005, file=file, original_exception=original_exception, message=message
)
class OracledbInsufficientPrivilegesError(OracledbCredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
20006, file=file, original_exception=original_exception, message=message
)
@@ -0,0 +1,71 @@
SENSITIVE_ARGUMENTS = frozenset({"--oracledb-password"})
def init_parser(self):
"""Init the Oracle Database Provider CLI parser.
The provider authenticates with a database user and password over a
python-oracledb thin-mode connection. The password should be supplied
via the `ORACLEDB_PASSWORD` environment variable; the flag accepts a
value only for backward compatibility and its value is redacted in
outputs.
"""
oracledb_parser = self.subparsers.add_parser(
"oracledb",
parents=[self.common_providers_parser],
help="Oracle Database Provider",
)
oracledb_auth_subparser = oracledb_parser.add_argument_group("Authentication")
oracledb_auth_subparser.add_argument(
"--oracledb-user",
nargs="?",
default=None,
metavar="ORACLEDB_USER",
help=(
"Oracle Database user to connect with. Needs read access to the "
"data dictionary (SELECT ANY DICTIONARY or SELECT_CATALOG_ROLE)."
),
)
oracledb_auth_subparser.add_argument(
"--oracledb-password",
nargs="?",
default=None,
metavar="ORACLEDB_PASSWORD",
help=(
"Password for the Oracle Database user. Use the ORACLEDB_PASSWORD "
"environment variable instead of passing the value directly."
),
)
oracledb_connection_subparser = oracledb_parser.add_argument_group("Connection")
oracledb_connection_subparser.add_argument(
"--oracledb-dsn",
nargs="?",
default=None,
metavar="ORACLEDB_DSN",
help=(
"Connect string, e.g. host:1521/service_name or a full Easy "
"Connect string. Takes precedence over --oracledb-host/"
"--oracledb-port/--oracledb-service-name."
),
)
oracledb_connection_subparser.add_argument(
"--oracledb-host",
nargs="?",
default=None,
metavar="ORACLEDB_HOST",
help="Database listener host, used with --oracledb-service-name.",
)
oracledb_connection_subparser.add_argument(
"--oracledb-port",
type=int,
default=None,
metavar="ORACLEDB_PORT",
help="Database listener port. Default: 1521.",
)
oracledb_connection_subparser.add_argument(
"--oracledb-service-name",
nargs="?",
default=None,
metavar="ORACLEDB_SERVICE_NAME",
help="Database service name, used with --oracledb-host.",
)
@@ -0,0 +1,16 @@
from prowler.lib.check.models import CheckReportOracledb
from prowler.lib.mutelist.mutelist import Mutelist
from prowler.lib.outputs.utils import unroll_dict, unroll_tags
class OracledbMutelist(Mutelist):
def is_finding_muted(
self, finding: CheckReportOracledb, database_name: str
) -> bool:
return self.is_muted(
database_name,
finding.check_metadata.CheckID,
"*",
finding.resource_name,
unroll_dict(unroll_tags(finding.resource_tags)),
)
@@ -0,0 +1,35 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from prowler.providers.oracledb.oracledb_provider import OracledbProvider
class OracledbService:
"""Base class for Oracle Database service implementations.
Every service shares the single authenticated python-oracledb connection
opened by the provider Oracle sessions are expensive and the data
dictionary queries the checks need are all read-only.
"""
def __init__(self, service: str, provider: "OracledbProvider"):
self.provider = provider
self.service = service
self.audit_config = provider.audit_config
self.fixer_config = provider.fixer_config
self.connection = provider.session.connection
self.database_name = provider.identity.database_name
def _execute_query(self, query: str, parameters: dict = None) -> list[tuple]:
"""Run a read-only data dictionary query and return every row.
Args:
query: The SQL statement to execute.
parameters: Optional bind variables for the statement.
Returns:
list[tuple]: All rows returned by the query.
"""
with self.connection.cursor() as cursor:
cursor.execute(query, parameters or {})
return cursor.fetchall()
+44
View File
@@ -0,0 +1,44 @@
from typing import Any, Optional
from pydantic import BaseModel, ConfigDict
from prowler.config.config import output_file_timestamp
from prowler.providers.common.models import ProviderOutputOptions
class OracledbSession(BaseModel):
"""Holds the live python-oracledb connection plus the non-secret
connection coordinates. The password is intentionally not stored
services only need the already-authenticated connection object."""
model_config = ConfigDict(arbitrary_types_allowed=True)
user: str
dsn: str
# python-oracledb Connection; Any so pydantic does not try to validate it.
connection: Any
class OracledbIdentityInfo(BaseModel):
user: str
dsn: str
# Database global name (GLOBAL_NAME view), e.g. ORCL.EXAMPLE.COM. Used as
# the account UID in outputs and as the mutelist account key.
database_name: str
# Full version string from PRODUCT_COMPONENT_VERSION; empty when the
# connected user cannot query it.
version: Optional[str] = ""
class OracledbOutputOptions(ProviderOutputOptions):
def __init__(self, arguments, bulk_checks_metadata, identity):
super().__init__(arguments, bulk_checks_metadata)
if (
not hasattr(arguments, "output_filename")
or arguments.output_filename is None
):
self.output_filename = (
f"prowler-output-{identity.database_name}-{output_file_timestamp}"
)
else:
self.output_filename = arguments.output_filename
@@ -0,0 +1,365 @@
import os
from os import environ
import oracledb
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.mutelist.mutelist import Mutelist
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.oracledb.exceptions.exceptions import (
OracledbConnectionError,
OracledbEnvironmentVariableError,
OracledbInvalidCredentialsError,
OracledbInvalidProviderIdError,
OracledbSetUpIdentityError,
OracledbSetUpSessionError,
)
from prowler.providers.oracledb.lib.mutelist.mutelist import OracledbMutelist
from prowler.providers.oracledb.models import OracledbIdentityInfo, OracledbSession
DEFAULT_PORT = 1521
class OracledbProvider(Provider):
"""Oracle Database Provider class.
Connects to a single Oracle Database (on-premises, in a VM or a cloud
managed service) with python-oracledb in thin mode no Oracle Client
libraries are required and runs security assessment checks inspired by
the Oracle Database Security Assessment Tool (DBSAT) against the data
dictionary (DBA_* and V$* views).
Attributes:
_type (str): The type of the provider.
_auth_method (str): The authentication method used by the provider.
_session (OracledbSession): The session object for the provider.
_identity (OracledbIdentityInfo): The identity information for the provider.
_audit_config (dict): The audit configuration for the provider.
_fixer_config (dict): The fixer configuration for the provider.
_mutelist (Mutelist): The mutelist for the provider.
audit_metadata (Audit_Metadata): The audit metadata for the provider.
"""
_type: str = "oracledb"
sdk_only: bool = False
_auth_method: str = "User / Password"
_session: OracledbSession
_identity: OracledbIdentityInfo
_audit_config: dict
_fixer_config: dict
_mutelist: Mutelist
audit_metadata: Audit_Metadata
def __init__(
self,
oracledb_user: str = "",
oracledb_password: str = "",
oracledb_dsn: str = "",
oracledb_host: str = "",
oracledb_port: int = None,
oracledb_service_name: str = "",
config_path: str = None,
config_content: dict = None,
fixer_config: dict = {},
mutelist_path: str = None,
mutelist_content: dict = None,
):
"""Oracle Database Provider constructor."""
logger.info("Instantiating Oracle Database Provider...")
OracledbProvider.validate_arguments(
oracledb_user=oracledb_user,
oracledb_password=oracledb_password,
oracledb_dsn=oracledb_dsn,
oracledb_host=oracledb_host,
oracledb_service_name=oracledb_service_name,
)
self._session = OracledbProvider.setup_session(
user=oracledb_user,
password=oracledb_password,
dsn=oracledb_dsn,
host=oracledb_host,
port=oracledb_port,
service_name=oracledb_service_name,
)
self._identity = OracledbProvider.setup_identity(self._session)
if config_content:
self._audit_config = config_content
else:
if not config_path:
config_path = default_config_file_path
self._audit_config = load_and_validate_config_file(self._type, config_path)
self._fixer_config = fixer_config
if mutelist_content:
self._mutelist = OracledbMutelist(mutelist_content=mutelist_content)
else:
if not mutelist_path:
mutelist_path = get_default_mute_file_path(self.type)
self._mutelist = OracledbMutelist(mutelist_path=mutelist_path)
Provider.set_global_provider(self)
@property
def auth_method(self):
return self._auth_method
@property
def session(self):
return self._session
@property
def identity(self):
return self._identity
@property
def type(self):
return self._type
@property
def audit_config(self):
return self._audit_config
@property
def fixer_config(self):
return self._fixer_config
@property
def mutelist(self) -> OracledbMutelist:
return self._mutelist
@staticmethod
def resolve_dsn(
dsn: str = "", host: str = "", port: int = None, service_name: str = ""
) -> str:
"""Return the connect string, built from host/port/service when no DSN
is given. An explicit DSN always wins so users can pass full Easy
Connect strings (including protocol and wallet options) untouched."""
dsn = dsn or environ.get("ORACLEDB_DSN", "")
if dsn:
return dsn.strip()
host = host or environ.get("ORACLEDB_HOST", "")
service_name = service_name or environ.get("ORACLEDB_SERVICE_NAME", "")
if host and service_name:
port = port or int(environ.get("ORACLEDB_PORT", DEFAULT_PORT))
return f"{host.strip()}:{port}/{service_name.strip()}"
return ""
@staticmethod
def validate_arguments(
oracledb_user: str = "",
oracledb_password: str = "",
oracledb_dsn: str = "",
oracledb_host: str = "",
oracledb_service_name: str = "",
):
"""Validate that all required connection values are provided.
Falls back to the matching `ORACLEDB_*` environment variables when a
CLI argument is not supplied. Raises a single combined error if any
required value is missing.
"""
user = oracledb_user or environ.get("ORACLEDB_USER", "")
password = oracledb_password or environ.get("ORACLEDB_PASSWORD", "")
dsn = OracledbProvider.resolve_dsn(
dsn=oracledb_dsn, host=oracledb_host, service_name=oracledb_service_name
)
missing = []
if not user:
missing.append("--oracledb-user / ORACLEDB_USER")
if not password:
missing.append("ORACLEDB_PASSWORD")
if not dsn:
missing.append(
"--oracledb-dsn / ORACLEDB_DSN (or --oracledb-host and "
"--oracledb-service-name)"
)
if missing:
raise OracledbEnvironmentVariableError(
file=os.path.basename(__file__),
message=(
"Oracle Database provider requires the connection "
"credentials. Missing: " + ", ".join(missing)
),
)
@staticmethod
def setup_session(
user: str = "",
password: str = "",
dsn: str = "",
host: str = "",
port: int = None,
service_name: str = "",
) -> OracledbSession:
"""Open a python-oracledb thin-mode connection from CLI args, falling
back to environment variables.
The password is read from `ORACLEDB_PASSWORD` when not supplied
secrets should be passed through environment variables, never CLI
values.
"""
try:
user = user or environ.get("ORACLEDB_USER", "")
password = password or environ.get("ORACLEDB_PASSWORD", "")
resolved_dsn = OracledbProvider.resolve_dsn(
dsn=dsn, host=host, port=port, service_name=service_name
)
connection = oracledb.connect(
user=user, password=password, dsn=resolved_dsn
)
return OracledbSession(user=user, dsn=resolved_dsn, connection=connection)
except oracledb.DatabaseError as error:
# ORA-01017: invalid username/password — a credential problem, not
# a connectivity one; keep the two remediation paths separate.
if "ORA-01017" in str(error):
raise OracledbInvalidCredentialsError(
file=os.path.basename(__file__),
original_exception=error,
message=f"Invalid Oracle Database credentials: {error}",
)
raise OracledbConnectionError(
file=os.path.basename(__file__),
original_exception=error,
message=f"Could not connect to Oracle Database '{dsn}': {error}",
)
except (OracledbInvalidCredentialsError, OracledbConnectionError):
raise
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
raise OracledbSetUpSessionError(original_exception=error)
@staticmethod
def setup_identity(session: OracledbSession) -> OracledbIdentityInfo:
"""Build the identity from the connected database.
The database global name (GLOBAL_NAME view, readable by any user)
identifies the audited database in outputs and the mutelist. The
version is best-effort: PRODUCT_COMPONENT_VERSION may be restricted,
in which case it is left empty rather than failing the scan.
"""
try:
with session.connection.cursor() as cursor:
cursor.execute("SELECT global_name FROM global_name")
database_name = cursor.fetchone()[0]
version = ""
try:
with session.connection.cursor() as cursor:
cursor.execute(
"SELECT version_full FROM product_component_version "
"WHERE product LIKE 'Oracle%'"
)
row = cursor.fetchone()
if row:
version = row[0]
except Exception as error:
logger.warning(
f"Could not read the Oracle Database version: "
f"{error.__class__.__name__}: {error}"
)
return OracledbIdentityInfo(
user=session.user,
dsn=session.dsn,
database_name=database_name,
version=version,
)
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
raise OracledbSetUpIdentityError(original_exception=error)
def print_credentials(self):
report_lines = [
f"Oracle Database: {Fore.YELLOW}{self.identity.database_name}{Style.RESET_ALL}",
f"DSN: {Fore.YELLOW}{self.identity.dsn}{Style.RESET_ALL}",
f"User: {Fore.YELLOW}{self.identity.user}{Style.RESET_ALL}",
]
if self.identity.version:
report_lines.append(
f"Version: {Fore.YELLOW}{self.identity.version}{Style.RESET_ALL}"
)
report_title = f"{Style.BRIGHT}Using the Oracle Database credentials below:{Style.RESET_ALL}"
print_boxes(report_lines, report_title)
@staticmethod
def test_connection(
oracledb_user: str = "",
oracledb_password: str = "",
oracledb_dsn: str = "",
oracledb_host: str = "",
oracledb_port: int = None,
oracledb_service_name: str = "",
raise_on_exception: bool = True,
provider_id: str = None,
) -> Connection:
"""Test the connection to an Oracle Database with the provided credentials.
Args:
provider_id: The provider ID (Oracle Database global name). When
supplied, the connected database global name must match it
guards against the stored provider UID drifting from the
database the credentials actually connect to. Compared
case-insensitively; GLOBAL_NAME is stored uppercase.
"""
try:
OracledbProvider.validate_arguments(
oracledb_user=oracledb_user,
oracledb_password=oracledb_password,
oracledb_dsn=oracledb_dsn,
oracledb_host=oracledb_host,
oracledb_service_name=oracledb_service_name,
)
session = OracledbProvider.setup_session(
user=oracledb_user,
password=oracledb_password,
dsn=oracledb_dsn,
host=oracledb_host,
port=oracledb_port,
service_name=oracledb_service_name,
)
try:
identity = OracledbProvider.setup_identity(session)
if (
provider_id
and provider_id.strip().upper() != identity.database_name.upper()
):
raise OracledbInvalidProviderIdError(
file=os.path.basename(__file__),
message=(
f"The provider ID '{provider_id}' does not match "
f"the connected Oracle Database global name "
f"'{identity.database_name}'."
),
)
finally:
try:
session.connection.close()
except Exception:
pass
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,4 @@
from prowler.providers.common.provider import Provider
from prowler.providers.oracledb.services.audit.audit_service import Audit
audit_client = Audit(Provider.get_global_provider())
@@ -0,0 +1,70 @@
from typing import Optional
from prowler.lib.logger import logger
from prowler.providers.oracledb.lib.service.service import OracledbService
from prowler.providers.oracledb.oracledb_provider import OracledbProvider
class Audit(OracledbService):
"""Oracle Database auditing service.
Reads the auditing initialization parameters, the Unified Auditing
option and the enabled unified audit policies, mirroring the auditing
findings of the Oracle Database Security Assessment Tool (DBSAT).
"""
def __init__(self, provider: OracledbProvider):
super().__init__(__class__.__name__, provider)
self.audit_trail: Optional[str] = None
self.audit_sys_operations: Optional[str] = None
self._get_audit_parameters()
self.unified_auditing = self._get_unified_auditing()
self.enabled_unified_policies = self._list_enabled_unified_policies()
def _get_audit_parameters(self):
"""Read the AUDIT_TRAIL and AUDIT_SYS_OPERATIONS parameters."""
logger.info("Audit - Reading auditing initialization parameters...")
try:
rows = self._execute_query(
"SELECT name, value FROM v$parameter "
"WHERE name IN ('audit_trail', 'audit_sys_operations')"
)
for name, value in rows:
if name == "audit_trail":
self.audit_trail = value
elif name == "audit_sys_operations":
self.audit_sys_operations = value
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _get_unified_auditing(self) -> bool:
"""Return True when the database runs with pure Unified Auditing."""
logger.info("Audit - Reading the Unified Auditing option...")
try:
rows = self._execute_query(
"SELECT value FROM v$option WHERE parameter = 'Unified Auditing'"
)
if rows:
return rows[0][0] == "TRUE"
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return False
def _list_enabled_unified_policies(self) -> list[str]:
"""List the enabled unified audit policies."""
logger.info("Audit - Listing enabled unified audit policies...")
policies = []
try:
rows = self._execute_query(
"SELECT DISTINCT policy_name FROM audit_unified_enabled_policies"
)
policies = sorted(row[0] for row in rows)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return policies
@@ -0,0 +1,38 @@
{
"Provider": "oracledb",
"CheckID": "audit_sys_operations_enabled",
"CheckTitle": "Auditing of SYS administrative operations is enabled",
"CheckType": [],
"ServiceName": "audit",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "OracleDatabase",
"ResourceGroup": "monitoring",
"Description": "Checks that **AUDIT_SYS_OPERATIONS** is TRUE so statements run by SYS and users connecting AS SYSDBA/SYSOPER are audited (DBSAT finding AUDIT.ADMINACTIONS).",
"Risk": "SYS bypasses standard auditing. Without AUDIT_SYS_OPERATIONS, the most privileged actions in the database — including tampering with the audit trail itself — leave no trace.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/introduction-to-auditing.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "ALTER SYSTEM SET audit_sys_operations=TRUE SCOPE=SPFILE;",
"NativeIaC": "",
"Other": "1. Run ALTER SYSTEM SET audit_sys_operations=TRUE SCOPE=SPFILE\n2. Restart the database\n3. Ship the generated OS audit files to a centralized, tamper-resistant log store",
"Terraform": ""
},
"Recommendation": {
"Text": "Set AUDIT_SYS_OPERATIONS to TRUE so administrative activity by SYS is written to the operating system audit trail, and forward those files to a SIEM.",
"Url": "https://hub.prowler.com/check/audit_sys_operations_enabled"
}
},
"Categories": [
"logging",
"forensics-ready"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,34 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.audit.audit_client import audit_client
class audit_sys_operations_enabled(Check):
"""Check that auditing of SYS administrative operations is enabled."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A single report for the audited database.
"""
findings = []
report = CheckReportOracledb(
metadata=self.metadata(),
resource={},
resource_name=audit_client.database_name,
resource_id=audit_client.database_name,
)
if (audit_client.audit_sys_operations or "").upper() == "TRUE":
report.status = "PASS"
report.status_extended = (
f"Database {audit_client.database_name} audits SYS "
"administrative operations (AUDIT_SYS_OPERATIONS=TRUE)."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Database {audit_client.database_name} does not audit SYS "
"administrative operations (AUDIT_SYS_OPERATIONS=FALSE)."
)
findings.append(report)
return findings
@@ -0,0 +1,38 @@
{
"Provider": "oracledb",
"CheckID": "audit_trail_enabled",
"CheckTitle": "Database auditing is enabled",
"CheckType": [],
"ServiceName": "audit",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "OracleDatabase",
"ResourceGroup": "monitoring",
"Description": "Checks that database auditing is active, either through pure **Unified Auditing** or a traditional **AUDIT_TRAIL** setting other than NONE (DBSAT finding AUDIT.ENABLED).",
"Risk": "With auditing disabled there is no record of logins, privilege use or data access. Attacks and misuse become undetectable, forensics is impossible and most regulatory frameworks are violated.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/introduction-to-auditing.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "ALTER SYSTEM SET audit_trail=DB,EXTENDED SCOPE=SPFILE;",
"NativeIaC": "",
"Other": "1. Prefer Unified Auditing: relink the database with the uniaud_on option or rely on mixed mode policies\n2. For traditional auditing, run ALTER SYSTEM SET audit_trail=DB,EXTENDED SCOPE=SPFILE and restart\n3. Enable at least the ORA_SECURECONFIG and ORA_LOGON_FAILURES unified audit policies",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable Unified Auditing (or at minimum set AUDIT_TRAIL to a value other than NONE) and enable baseline audit policies so security-relevant activity is recorded.",
"Url": "https://hub.prowler.com/check/audit_trail_enabled"
}
},
"Categories": [
"logging",
"forensics-ready"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,41 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.audit.audit_client import audit_client
class audit_trail_enabled(Check):
"""Check that database auditing is enabled."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A single report for the audited database.
"""
findings = []
report = CheckReportOracledb(
metadata=self.metadata(),
resource={},
resource_name=audit_client.database_name,
resource_id=audit_client.database_name,
)
audit_trail = (audit_client.audit_trail or "NONE").upper()
if audit_client.unified_auditing:
report.status = "PASS"
report.status_extended = (
f"Database {audit_client.database_name} has Unified "
"Auditing enabled."
)
elif audit_trail != "NONE":
report.status = "PASS"
report.status_extended = (
f"Database {audit_client.database_name} has traditional "
f"auditing enabled (AUDIT_TRAIL={audit_trail})."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Database {audit_client.database_name} has auditing "
"disabled (AUDIT_TRAIL=NONE and no Unified Auditing)."
)
findings.append(report)
return findings
@@ -0,0 +1,6 @@
from prowler.providers.common.provider import Provider
from prowler.providers.oracledb.services.configuration.configuration_service import (
Configuration,
)
configuration_client = Configuration(Provider.get_global_provider())
@@ -0,0 +1,38 @@
{
"Provider": "oracledb",
"CheckID": "configuration_dictionary_accessibility_disabled",
"CheckTitle": "Access to data dictionary objects is restricted (O7_DICTIONARY_ACCESSIBILITY is FALSE)",
"CheckType": [],
"ServiceName": "configuration",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "OracleDatabaseParameter",
"ResourceGroup": "database",
"Description": "Checks that **O7_DICTIONARY_ACCESSIBILITY** is FALSE so ANY-style object privileges (SELECT ANY TABLE, etc.) do not extend to SYS-owned data dictionary objects (DBSAT finding CONF.SYSOBJ).",
"Risk": "With the parameter set to TRUE, users holding ANY privileges can read or modify dictionary base tables — including password verifiers — enabling credential theft and full database compromise.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/O7_DICTIONARY_ACCESSIBILITY.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "ALTER SYSTEM SET o7_dictionary_accessibility=FALSE SCOPE=SPFILE;",
"NativeIaC": "",
"Other": "1. Run ALTER SYSTEM SET o7_dictionary_accessibility=FALSE SCOPE=SPFILE\n2. Restart the database\n3. Grant SELECT_CATALOG_ROLE or specific dictionary views to users who genuinely need dictionary access",
"Terraform": ""
},
"Recommendation": {
"Text": "Keep O7_DICTIONARY_ACCESSIBILITY at its default FALSE and give dictionary access through SELECT_CATALOG_ROLE or explicit view grants instead.",
"Url": "https://hub.prowler.com/check/configuration_dictionary_accessibility_disabled"
}
},
"Categories": [
"trust-boundaries",
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,36 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.configuration.configuration_client import (
configuration_client,
)
class configuration_dictionary_accessibility_disabled(Check):
"""Check that access to data dictionary objects is restricted."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A single report for the audited database, or none when the
initialization parameters could not be read.
"""
findings = []
if not configuration_client.parameters:
return findings
report = CheckReportOracledb(
metadata=self.metadata(),
resource={},
resource_name="o7_dictionary_accessibility",
resource_id=f"{configuration_client.database_name}/o7_dictionary_accessibility",
)
value = (
configuration_client.parameters.get("o7_dictionary_accessibility") or ""
).upper()
if value != "TRUE":
report.status = "PASS"
report.status_extended = f"Database {configuration_client.database_name} restricts access to data dictionary objects (O7_DICTIONARY_ACCESSIBILITY=FALSE)."
else:
report.status = "FAIL"
report.status_extended = f"Database {configuration_client.database_name} allows ANY-style privileges on data dictionary objects (O7_DICTIONARY_ACCESSIBILITY=TRUE)."
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "oracledb",
"CheckID": "configuration_remote_login_passwordfile_secure",
"CheckTitle": "The password file is not shared between databases",
"CheckType": [],
"ServiceName": "configuration",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "OracleDatabaseParameter",
"ResourceGroup": "IAM",
"Description": "Checks that **REMOTE_LOGIN_PASSWORDFILE** is EXCLUSIVE (or NONE), so the password file that stores SYSDBA/SYSOPER credentials is dedicated to this database and not SHARED (DBSAT finding CONF.PASSWORDFILE).",
"Risk": "A SHARED password file is reused by several databases and only supports SYS, encouraging shared administrative credentials: one compromised database exposes administrative access to all of them.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/REMOTE_LOGIN_PASSWORDFILE.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "ALTER SYSTEM SET remote_login_passwordfile=EXCLUSIVE SCOPE=SPFILE;",
"NativeIaC": "",
"Other": "1. Run ALTER SYSTEM SET remote_login_passwordfile=EXCLUSIVE SCOPE=SPFILE\n2. Restart the database\n3. Recreate the password file with orapwd if it was shared, granting administrative privileges per named user",
"Terraform": ""
},
"Recommendation": {
"Text": "Use an EXCLUSIVE password file (or NONE to disable remote administrative logins entirely) so administrative credentials are per-database and per-user.",
"Url": "https://hub.prowler.com/check/configuration_remote_login_passwordfile_secure"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,36 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.configuration.configuration_client import (
configuration_client,
)
class configuration_remote_login_passwordfile_secure(Check):
"""Check that the password file is not shared between databases."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A single report for the audited database, or none when the
initialization parameters could not be read.
"""
findings = []
if not configuration_client.parameters:
return findings
report = CheckReportOracledb(
metadata=self.metadata(),
resource={},
resource_name="remote_login_passwordfile",
resource_id=f"{configuration_client.database_name}/remote_login_passwordfile",
)
value = (
configuration_client.parameters.get("remote_login_passwordfile") or ""
).upper()
if value in ("EXCLUSIVE", "NONE"):
report.status = "PASS"
report.status_extended = f"Database {configuration_client.database_name} uses a dedicated password file (REMOTE_LOGIN_PASSWORDFILE={value})."
else:
report.status = "FAIL"
report.status_extended = f"Database {configuration_client.database_name} shares its password file with other databases (REMOTE_LOGIN_PASSWORDFILE={value})."
findings.append(report)
return findings
@@ -0,0 +1,38 @@
{
"Provider": "oracledb",
"CheckID": "configuration_remote_os_authentication_disabled",
"CheckTitle": "Remote operating system authentication is disabled",
"CheckType": [],
"ServiceName": "configuration",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "OracleDatabaseParameter",
"ResourceGroup": "IAM",
"Description": "Checks that **REMOTE_OS_AUTHENT** is FALSE (or absent — the parameter was desupported in 12c) so the database never trusts the client machine's OS username for authentication (DBSAT finding CONF.EXTERNALOSAUTH).",
"Risk": "With remote OS authentication enabled, any attacker who controls a client machine can create a local OS user matching an externally identified database account and log in without a password.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/REMOTE_OS_AUTHENT.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "ALTER SYSTEM SET remote_os_authent=FALSE SCOPE=SPFILE;",
"NativeIaC": "",
"Other": "1. Run ALTER SYSTEM SET remote_os_authent=FALSE SCOPE=SPFILE\n2. Restart the database\n3. Convert IDENTIFIED EXTERNALLY accounts that relied on it to password, Kerberos or certificate authentication",
"Terraform": ""
},
"Recommendation": {
"Text": "Keep REMOTE_OS_AUTHENT disabled and use strong, centrally managed authentication (passwords with profiles, Kerberos or TLS certificates) for remote users.",
"Url": "https://hub.prowler.com/check/configuration_remote_os_authentication_disabled"
}
},
"Categories": [
"identity-access",
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,34 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.configuration.configuration_client import (
configuration_client,
)
class configuration_remote_os_authentication_disabled(Check):
"""Check that remote operating system authentication is disabled."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A single report for the audited database, or none when the
initialization parameters could not be read.
"""
findings = []
if not configuration_client.parameters:
return findings
report = CheckReportOracledb(
metadata=self.metadata(),
resource={},
resource_name="remote_os_authent",
resource_id=f"{configuration_client.database_name}/remote_os_authent",
)
value = (configuration_client.parameters.get("remote_os_authent") or "").upper()
if value != "TRUE":
report.status = "PASS"
report.status_extended = f"Database {configuration_client.database_name} does not trust client operating system authentication (REMOTE_OS_AUTHENT=FALSE)."
else:
report.status = "FAIL"
report.status_extended = f"Database {configuration_client.database_name} trusts client operating system authentication (REMOTE_OS_AUTHENT=TRUE)."
findings.append(report)
return findings
@@ -0,0 +1,38 @@
{
"Provider": "oracledb",
"CheckID": "configuration_remote_os_roles_disabled",
"CheckTitle": "Remote operating system role management is disabled",
"CheckType": [],
"ServiceName": "configuration",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "OracleDatabaseParameter",
"ResourceGroup": "IAM",
"Description": "Checks that **REMOTE_OS_ROLES** is FALSE so database roles are never enabled based on the operating system groups of a remote client (DBSAT finding CONF.EXTERNALOSAUTH).",
"Risk": "With remote OS roles enabled, an attacker controlling a client machine can grant themselves OS groups that map to powerful database roles, escalating privileges without any database-side change.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/REMOTE_OS_ROLES.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "ALTER SYSTEM SET remote_os_roles=FALSE SCOPE=SPFILE;",
"NativeIaC": "",
"Other": "1. Run ALTER SYSTEM SET remote_os_roles=FALSE SCOPE=SPFILE\n2. Restart the database\n3. Manage roles inside the database (GRANT/SET ROLE) or through a central directory instead",
"Terraform": ""
},
"Recommendation": {
"Text": "Keep REMOTE_OS_ROLES at its default FALSE and manage role grants inside the database or via enterprise user security.",
"Url": "https://hub.prowler.com/check/configuration_remote_os_roles_disabled"
}
},
"Categories": [
"identity-access",
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,34 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.configuration.configuration_client import (
configuration_client,
)
class configuration_remote_os_roles_disabled(Check):
"""Check that remote operating system role management is disabled."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A single report for the audited database, or none when the
initialization parameters could not be read.
"""
findings = []
if not configuration_client.parameters:
return findings
report = CheckReportOracledb(
metadata=self.metadata(),
resource={},
resource_name="remote_os_roles",
resource_id=f"{configuration_client.database_name}/remote_os_roles",
)
value = (configuration_client.parameters.get("remote_os_roles") or "").upper()
if value != "TRUE":
report.status = "PASS"
report.status_extended = f"Database {configuration_client.database_name} does not enable roles based on remote client operating system groups (REMOTE_OS_ROLES=FALSE)."
else:
report.status = "FAIL"
report.status_extended = f"Database {configuration_client.database_name} enables roles based on remote client operating system groups (REMOTE_OS_ROLES=TRUE)."
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "oracledb",
"CheckID": "configuration_sec_return_server_release_banner_disabled",
"CheckTitle": "The server release banner is not returned to clients before authentication",
"CheckType": [],
"ServiceName": "configuration",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "OracleDatabaseParameter",
"ResourceGroup": "database",
"Description": "Checks that **SEC_RETURN_SERVER_RELEASE_BANNER** is FALSE so the database does not disclose its full release information to unauthenticated clients (DBSAT finding CONF.NETWORK).",
"Risk": "The full release banner tells attackers the exact patch level of the database, letting them pick known CVEs and exploits that match the unpatched version.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/SEC_RETURN_SERVER_RELEASE_BANNER.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "ALTER SYSTEM SET sec_return_server_release_banner=FALSE SCOPE=SPFILE;",
"NativeIaC": "",
"Other": "1. Run ALTER SYSTEM SET sec_return_server_release_banner=FALSE SCOPE=SPFILE\n2. Restart the database",
"Terraform": ""
},
"Recommendation": {
"Text": "Keep SEC_RETURN_SERVER_RELEASE_BANNER at its default FALSE so only a generic version is disclosed before authentication.",
"Url": "https://hub.prowler.com/check/configuration_sec_return_server_release_banner_disabled"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,37 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.configuration.configuration_client import (
configuration_client,
)
class configuration_sec_return_server_release_banner_disabled(Check):
"""Check that the server release banner is not returned to clients."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A single report for the audited database, or none when the
initialization parameters could not be read.
"""
findings = []
if not configuration_client.parameters:
return findings
report = CheckReportOracledb(
metadata=self.metadata(),
resource={},
resource_name="sec_return_server_release_banner",
resource_id=f"{configuration_client.database_name}/sec_return_server_release_banner",
)
value = (
configuration_client.parameters.get("sec_return_server_release_banner")
or ""
).upper()
if value != "TRUE":
report.status = "PASS"
report.status_extended = f"Database {configuration_client.database_name} does not return the full release banner to unauthenticated clients (SEC_RETURN_SERVER_RELEASE_BANNER=FALSE)."
else:
report.status = "FAIL"
report.status_extended = f"Database {configuration_client.database_name} returns the full release banner to unauthenticated clients (SEC_RETURN_SERVER_RELEASE_BANNER=TRUE)."
findings.append(report)
return findings
@@ -0,0 +1,49 @@
from prowler.lib.logger import logger
from prowler.providers.oracledb.lib.service.service import OracledbService
from prowler.providers.oracledb.oracledb_provider import OracledbProvider
# Security-relevant initialization parameters evaluated by the configuration
# checks (DBSAT CONF.* findings).
SECURITY_PARAMETERS = (
"o7_dictionary_accessibility",
"sql92_security",
"remote_os_authent",
"remote_os_roles",
"remote_login_passwordfile",
"sec_return_server_release_banner",
)
class Configuration(OracledbService):
"""Oracle Database configuration service.
Reads security-relevant initialization parameters from V$PARAMETER,
mirroring the database configuration findings of the Oracle Database
Security Assessment Tool (DBSAT).
"""
def __init__(self, provider: OracledbProvider):
super().__init__(__class__.__name__, provider)
self.parameters = self._get_parameters()
def _get_parameters(self) -> dict:
"""Read the security-relevant initialization parameters.
Returns:
dict: {parameter_name: value}. Parameters removed in newer
releases (e.g. REMOTE_OS_AUTHENT was desupported in 12c) are
simply absent; checks treat absence as the secure default.
"""
logger.info("Configuration - Reading initialization parameters...")
parameters = {}
try:
in_list = ", ".join(f"'{parameter}'" for parameter in SECURITY_PARAMETERS)
rows = self._execute_query(
f"SELECT name, value FROM v$parameter WHERE name IN ({in_list})"
)
parameters = {name: value for name, value in rows}
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return parameters
@@ -0,0 +1,37 @@
{
"Provider": "oracledb",
"CheckID": "configuration_sql92_security_enabled",
"CheckTitle": "SQL92 security is enabled for UPDATE and DELETE statements",
"CheckType": [],
"ServiceName": "configuration",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "OracleDatabaseParameter",
"ResourceGroup": "database",
"Description": "Checks that **SQL92_SECURITY** is TRUE so UPDATE and DELETE statements whose WHERE clause reads a column also require SELECT privilege on that column (DBSAT finding CONF.SQL92SECURITY).",
"Risk": "With SQL92_SECURITY disabled, a user with only UPDATE or DELETE privilege can infer column values they cannot SELECT by observing which rows a conditional statement affects — a data inference attack.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/SQL92_SECURITY.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "ALTER SYSTEM SET sql92_security=TRUE SCOPE=SPFILE;",
"NativeIaC": "",
"Other": "1. Run ALTER SYSTEM SET sql92_security=TRUE SCOPE=SPFILE\n2. Restart the database\n3. Verify applications that rely on UPDATE/DELETE without SELECT still hold the required privileges",
"Terraform": ""
},
"Recommendation": {
"Text": "Set SQL92_SECURITY to TRUE (the default since Oracle 12.2) so conditional UPDATE and DELETE statements cannot be used to infer protected data.",
"Url": "https://hub.prowler.com/check/configuration_sql92_security_enabled"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,34 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.configuration.configuration_client import (
configuration_client,
)
class configuration_sql92_security_enabled(Check):
"""Check that SQL92 security is enabled for UPDATE/DELETE statements."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A single report for the audited database, or none when the
initialization parameters could not be read.
"""
findings = []
if not configuration_client.parameters:
return findings
report = CheckReportOracledb(
metadata=self.metadata(),
resource={},
resource_name="sql92_security",
resource_id=f"{configuration_client.database_name}/sql92_security",
)
value = (configuration_client.parameters.get("sql92_security") or "").upper()
if value == "TRUE":
report.status = "PASS"
report.status_extended = f"Database {configuration_client.database_name} requires SELECT privilege on columns referenced by conditional UPDATE/DELETE statements (SQL92_SECURITY=TRUE)."
else:
report.status = "FAIL"
report.status_extended = f"Database {configuration_client.database_name} allows data inference through conditional UPDATE/DELETE statements (SQL92_SECURITY=FALSE)."
findings.append(report)
return findings
@@ -0,0 +1,6 @@
from prowler.providers.common.provider import Provider
from prowler.providers.oracledb.services.encryption.encryption_service import (
Encryption,
)
encryption_client = Encryption(Provider.get_global_provider())
@@ -0,0 +1,94 @@
from pydantic.v1 import BaseModel
from prowler.lib.logger import logger
from prowler.providers.oracledb.lib.service.service import OracledbService
from prowler.providers.oracledb.oracledb_provider import OracledbProvider
# Oracle-managed tablespaces excluded from the TDE tablespace check: they hold
# the data dictionary or transient data and are reported separately by DBSAT.
SYSTEM_TABLESPACES = frozenset({"SYSTEM", "SYSAUX"})
class Encryption(OracledbService):
"""Oracle Database encryption service.
Reads V$ENCRYPTION_WALLET and DBA_TABLESPACES to evaluate Transparent
Data Encryption (TDE) posture, mirroring the encryption findings of the
Oracle Database Security Assessment Tool (DBSAT).
"""
def __init__(self, provider: OracledbProvider):
super().__init__(__class__.__name__, provider)
self.wallets = self._list_wallets()
self.tablespaces = self._list_tablespaces()
def _list_wallets(self) -> list["Wallet"]:
"""List the TDE keystore(s) configured for the database."""
logger.info("Encryption - Listing TDE keystores...")
wallets = []
try:
rows = self._execute_query(
"SELECT wrl_type, wrl_parameter, status, wallet_type "
"FROM v$encryption_wallet"
)
for wrl_type, wrl_parameter, status, wallet_type in rows:
wallets.append(
Wallet(
wrl_type=wrl_type or "",
wrl_parameter=wrl_parameter or "",
status=status or "",
wallet_type=wallet_type or "",
)
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return wallets
def _list_tablespaces(self) -> list["Tablespace"]:
"""List permanent tablespaces and their encryption status."""
logger.info("Encryption - Listing tablespaces...")
tablespaces = []
try:
rows = self._execute_query(
"SELECT tablespace_name, encrypted, contents FROM dba_tablespaces"
)
for name, encrypted, contents in rows:
tablespaces.append(
Tablespace(
name=name,
encrypted=encrypted == "YES",
contents=contents or "",
)
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
logger.info(f"Found {len(tablespaces)} tablespaces")
return tablespaces
class Wallet(BaseModel):
"""Oracle Database TDE keystore model (V$ENCRYPTION_WALLET row)."""
wrl_type: str = ""
wrl_parameter: str = ""
status: str = ""
wallet_type: str = ""
class Tablespace(BaseModel):
"""Oracle Database tablespace model."""
name: str
encrypted: bool = False
contents: str = ""
@property
def is_user_permanent(self) -> bool:
"""True for permanent, non-Oracle-managed tablespaces — the ones the
TDE check evaluates. Temporary and undo tablespaces hold transient
data and follow the encryption of the data they stage."""
return self.contents == "PERMANENT" and self.name not in SYSTEM_TABLESPACES
@@ -0,0 +1,37 @@
{
"Provider": "oracledb",
"CheckID": "encryption_tablespaces_encrypted",
"CheckTitle": "Permanent tablespaces are encrypted with TDE",
"CheckType": [],
"ServiceName": "encryption",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "OracleDatabaseTablespace",
"ResourceGroup": "storage",
"Description": "Checks that each permanent application tablespace is encrypted with **Transparent Data Encryption (TDE)**. SYSTEM and SYSAUX are excluded and reported separately by DBSAT (finding ENCRYPT.TDE).",
"Risk": "Unencrypted tablespaces expose their data to anyone who can read the underlying files: OS administrators, stolen backups or disks, and direct data file parsing all bypass database authentication entirely.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/asoag/encrypting-tablespaces.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "CREATE TABLESPACE <name> DATAFILE ... ENCRYPTION USING 'AES256' DEFAULT STORAGE (ENCRYPT);",
"NativeIaC": "",
"Other": "1. Configure and open the TDE keystore\n2. Encrypt existing tablespaces online: ALTER TABLESPACE <name> ENCRYPTION ONLINE USING 'AES256' ENCRYPT\n3. Create new tablespaces encrypted by default (ENCRYPT_NEW_TABLESPACES=ALWAYS)",
"Terraform": ""
},
"Recommendation": {
"Text": "Encrypt all permanent tablespaces with TDE, use online conversion for existing ones and set ENCRYPT_NEW_TABLESPACES so future tablespaces are encrypted automatically.",
"Url": "https://hub.prowler.com/check/encryption_tablespaces_encrypted"
}
},
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,37 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.encryption.encryption_client import (
encryption_client,
)
class encryption_tablespaces_encrypted(Check):
"""Check that permanent tablespaces are encrypted with TDE."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A list of reports, one per permanent application tablespace.
"""
findings = []
for tablespace in encryption_client.tablespaces:
if not tablespace.is_user_permanent:
continue
report = CheckReportOracledb(
metadata=self.metadata(),
resource=tablespace,
resource_name=tablespace.name,
resource_id=tablespace.name,
)
if tablespace.encrypted:
report.status = "PASS"
report.status_extended = (
f"Tablespace {tablespace.name} is encrypted with TDE."
)
else:
report.status = "FAIL"
report.status_extended = (
f"Tablespace {tablespace.name} is not encrypted with TDE."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "oracledb",
"CheckID": "encryption_wallet_configured",
"CheckTitle": "A TDE keystore is configured and open",
"CheckType": [],
"ServiceName": "encryption",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "OracleDatabase",
"ResourceGroup": "security",
"Description": "Checks that a Transparent Data Encryption (TDE) keystore (wallet) is configured and **OPEN**, so the database can encrypt tablespaces and columns (DBSAT finding ENCRYPT.WALLET).",
"Risk": "Without a configured TDE keystore no data-at-rest encryption is possible: stolen data files, backups or disks can be read directly with standard tools, bypassing every access control in the database.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/asoag/configuring-transparent-data-encryption.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "ADMINISTER KEY MANAGEMENT CREATE KEYSTORE IDENTIFIED BY <password>;",
"NativeIaC": "",
"Other": "1. Set WALLET_ROOT and TDE_CONFIGURATION initialization parameters\n2. Create the keystore: ADMINISTER KEY MANAGEMENT CREATE KEYSTORE IDENTIFIED BY <password>\n3. Open it and set a master key: ADMINISTER KEY MANAGEMENT SET KEY IDENTIFIED BY <password> WITH BACKUP\n4. Consider an auto-login keystore for unattended restarts",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure a TDE keystore, set a master encryption key and keep the keystore open (auto-login where appropriate) so tablespaces and backups can be encrypted.",
"Url": "https://hub.prowler.com/check/encryption_wallet_configured"
}
},
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,46 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.encryption.encryption_client import (
encryption_client,
)
class encryption_wallet_configured(Check):
"""Check that a TDE keystore is configured and open."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A single report for the audited database.
"""
findings = []
report = CheckReportOracledb(
metadata=self.metadata(),
resource={},
resource_name=encryption_client.database_name,
resource_id=encryption_client.database_name,
)
open_wallets = [
wallet for wallet in encryption_client.wallets if wallet.status == "OPEN"
]
if open_wallets:
report.status = "PASS"
report.status_extended = (
f"Database {encryption_client.database_name} has an open TDE "
f"keystore ({open_wallets[0].wrl_type}, "
f"{open_wallets[0].wallet_type})."
)
else:
statuses = (
", ".join(
sorted({wallet.status for wallet in encryption_client.wallets})
)
or "NOT_AVAILABLE"
)
report.status = "FAIL"
report.status_extended = (
f"Database {encryption_client.database_name} does not have an "
f"open TDE keystore (status: {statuses})."
)
findings.append(report)
return findings
@@ -0,0 +1,6 @@
from prowler.providers.common.provider import Provider
from prowler.providers.oracledb.services.privileges.privileges_service import (
Privileges,
)
privileges_client = Privileges(Provider.get_global_provider())
@@ -0,0 +1,38 @@
{
"Provider": "oracledb",
"CheckID": "privileges_public_no_encryption_packages",
"CheckTitle": "Encryption PL/SQL packages are not executable by PUBLIC",
"CheckType": [],
"ServiceName": "privileges",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "OracleDatabase",
"ResourceGroup": "security",
"Description": "Checks that encryption PL/SQL packages (**DBMS_CRYPTO, DBMS_OBFUSCATION_TOOLKIT, DBMS_RANDOM**) are not granted EXECUTE to PUBLIC (DBSAT finding PRIV.ENCRYPTPACKAGEPUBLIC).",
"Risk": "Broad access to encryption primitives lets attackers encrypt stolen data to defeat inspection, build ransomware-style attacks inside the database, or misuse weak random generation in security-relevant code.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/managing-security-for-application-developers.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "REVOKE EXECUTE ON DBMS_CRYPTO FROM PUBLIC;",
"NativeIaC": "",
"Other": "1. List PUBLIC execute grants on the encryption packages in DBA_TAB_PRIVS\n2. Revoke each: REVOKE EXECUTE ON <package> FROM PUBLIC\n3. Grant EXECUTE only to the schemas whose code performs cryptographic operations",
"Terraform": ""
},
"Recommendation": {
"Text": "Revoke PUBLIC execute on the encryption packages and grant them only to the application schemas that legitimately perform cryptography.",
"Url": "https://hub.prowler.com/check/privileges_public_no_encryption_packages"
}
},
"Categories": [
"encryption",
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,43 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.privileges.privileges_client import (
privileges_client,
)
from prowler.providers.oracledb.services.privileges.privileges_service import (
ENCRYPTION_PACKAGES,
)
class privileges_public_no_encryption_packages(Check):
"""Check that encryption PL/SQL packages are not executable by PUBLIC."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A single report for the audited database.
"""
findings = []
report = CheckReportOracledb(
metadata=self.metadata(),
resource={},
resource_name=privileges_client.database_name,
resource_id=privileges_client.database_name,
)
exposed_packages = sorted(
set(privileges_client.public_execute_packages) & ENCRYPTION_PACKAGES
)
if exposed_packages:
report.status = "FAIL"
report.status_extended = (
f"Database {privileges_client.database_name} grants PUBLIC "
f"execute on encryption packages: "
f"{', '.join(exposed_packages)}."
)
else:
report.status = "PASS"
report.status_extended = (
f"Database {privileges_client.database_name} does not grant "
f"PUBLIC execute on any encryption package."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "oracledb",
"CheckID": "privileges_public_no_file_packages",
"CheckTitle": "File system PL/SQL packages are not executable by PUBLIC",
"CheckType": [],
"ServiceName": "privileges",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "OracleDatabase",
"ResourceGroup": "storage",
"Description": "Checks that file system PL/SQL packages (**UTL_FILE, DBMS_LOB, DBMS_ADVISOR**) are not granted EXECUTE to PUBLIC, limiting who can read and write files on the database server (DBSAT finding PRIV.FILESYSTEMPACKAGEPUBLIC).",
"Risk": "Any user able to run these packages can read or overwrite files accessible through directory objects — including data files, scripts and wallets — leading to data theft, tampering or code execution on the server.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/managing-security-for-application-developers.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "REVOKE EXECUTE ON UTL_FILE FROM PUBLIC;",
"NativeIaC": "",
"Other": "1. List PUBLIC execute grants on the file packages in DBA_TAB_PRIVS\n2. Revoke each: REVOKE EXECUTE ON <package> FROM PUBLIC\n3. Grant EXECUTE only to the schemas that need it and restrict directory objects to the minimum paths required",
"Terraform": ""
},
"Recommendation": {
"Text": "Revoke PUBLIC execute on the file system packages and control file access through per-user grants and tightly scoped directory objects.",
"Url": "https://hub.prowler.com/check/privileges_public_no_file_packages"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,43 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.privileges.privileges_client import (
privileges_client,
)
from prowler.providers.oracledb.services.privileges.privileges_service import (
FILE_PACKAGES,
)
class privileges_public_no_file_packages(Check):
"""Check that file system PL/SQL packages are not executable by PUBLIC."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A single report for the audited database.
"""
findings = []
report = CheckReportOracledb(
metadata=self.metadata(),
resource={},
resource_name=privileges_client.database_name,
resource_id=privileges_client.database_name,
)
exposed_packages = sorted(
set(privileges_client.public_execute_packages) & FILE_PACKAGES
)
if exposed_packages:
report.status = "FAIL"
report.status_extended = (
f"Database {privileges_client.database_name} grants PUBLIC "
f"execute on file system packages: "
f"{', '.join(exposed_packages)}."
)
else:
report.status = "PASS"
report.status_extended = (
f"Database {privileges_client.database_name} does not grant "
f"PUBLIC execute on any file system package."
)
findings.append(report)
return findings
@@ -0,0 +1,38 @@
{
"Provider": "oracledb",
"CheckID": "privileges_public_no_network_packages",
"CheckTitle": "Network PL/SQL packages are not executable by PUBLIC",
"CheckType": [],
"ServiceName": "privileges",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "OracleDatabase",
"ResourceGroup": "network",
"Description": "Checks that network PL/SQL packages (**UTL_HTTP, UTL_TCP, UTL_SMTP, UTL_MAIL, UTL_INADDR, DBMS_LDAP**) are not granted EXECUTE to PUBLIC, limiting who can open network connections from inside the database (DBSAT finding PRIV.NETPACKAGEPUBLIC).",
"Risk": "Any user able to run these packages can exfiltrate data over HTTP/TCP/SMTP, scan internal networks from the database host and reach services behind the database's network position.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/managing-fine-grained-access-in-pl-sql-packages-and-types.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "REVOKE EXECUTE ON UTL_HTTP FROM PUBLIC;",
"NativeIaC": "",
"Other": "1. List PUBLIC execute grants on the network packages in DBA_TAB_PRIVS\n2. Revoke each: REVOKE EXECUTE ON <package> FROM PUBLIC\n3. Grant EXECUTE to the specific application schemas that need it and define network ACLs (DBMS_NETWORK_ACL_ADMIN) for those users",
"Terraform": ""
},
"Recommendation": {
"Text": "Revoke PUBLIC execute on the network packages and combine per-user grants with fine-grained network ACLs so only authorized code can open outbound connections.",
"Url": "https://hub.prowler.com/check/privileges_public_no_network_packages"
}
},
"Categories": [
"trust-boundaries",
"internet-exposed"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,43 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.privileges.privileges_client import (
privileges_client,
)
from prowler.providers.oracledb.services.privileges.privileges_service import (
NETWORK_PACKAGES,
)
class privileges_public_no_network_packages(Check):
"""Check that network PL/SQL packages are not executable by PUBLIC."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A single report for the audited database.
"""
findings = []
report = CheckReportOracledb(
metadata=self.metadata(),
resource={},
resource_name=privileges_client.database_name,
resource_id=privileges_client.database_name,
)
exposed_packages = sorted(
set(privileges_client.public_execute_packages) & NETWORK_PACKAGES
)
if exposed_packages:
report.status = "FAIL"
report.status_extended = (
f"Database {privileges_client.database_name} grants PUBLIC "
f"execute on network packages: "
f"{', '.join(exposed_packages)}."
)
else:
report.status = "PASS"
report.status_extended = (
f"Database {privileges_client.database_name} does not grant "
f"PUBLIC execute on any network package."
)
findings.append(report)
return findings
@@ -0,0 +1,38 @@
{
"Provider": "oracledb",
"CheckID": "privileges_public_no_system_privileges",
"CheckTitle": "No system privileges are granted to PUBLIC",
"CheckType": [],
"ServiceName": "privileges",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "OracleDatabase",
"ResourceGroup": "IAM",
"Description": "Checks that no system privileges are granted to the **PUBLIC** role. Anything granted to PUBLIC is available to every database user, so system privileges must never be granted to it (DBSAT finding PRIV.SYSPUBLIC).",
"Risk": "A system privilege granted to PUBLIC is effectively granted to every current and future user, including compromised low-privilege accounts, enabling privilege escalation and broad unauthorized access.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/configuring-privilege-and-role-authorization.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "REVOKE <privilege> FROM PUBLIC;",
"NativeIaC": "",
"Other": "1. List the grants: SELECT privilege FROM dba_sys_privs WHERE grantee = 'PUBLIC'\n2. Revoke each one: REVOKE <privilege> FROM PUBLIC\n3. Grant the privilege to the specific users or roles that need it",
"Terraform": ""
},
"Recommendation": {
"Text": "Revoke all system privileges from PUBLIC and grant them only to the specific users or roles that require them.",
"Url": "https://hub.prowler.com/check/privileges_public_no_system_privileges"
}
},
"Categories": [
"identity-access",
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,37 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.privileges.privileges_client import (
privileges_client,
)
class privileges_public_no_system_privileges(Check):
"""Check that no system privileges are granted to PUBLIC."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A single report for the audited database.
"""
findings = []
report = CheckReportOracledb(
metadata=self.metadata(),
resource={},
resource_name=privileges_client.database_name,
resource_id=privileges_client.database_name,
)
public_privileges = privileges_client.public_system_privileges
if public_privileges:
report.status = "FAIL"
report.status_extended = (
f"Database {privileges_client.database_name} grants system "
f"privileges to PUBLIC: {', '.join(public_privileges)}."
)
else:
report.status = "PASS"
report.status_extended = (
f"Database {privileges_client.database_name} does not grant "
"any system privilege to PUBLIC."
)
findings.append(report)
return findings
@@ -0,0 +1,65 @@
from prowler.lib.logger import logger
from prowler.providers.oracledb.lib.service.service import OracledbService
from prowler.providers.oracledb.oracledb_provider import OracledbProvider
# SYS-owned PL/SQL packages that DBSAT flags when they are executable by
# PUBLIC, grouped by the DBSAT finding they belong to.
NETWORK_PACKAGES = frozenset(
{"UTL_HTTP", "UTL_TCP", "UTL_SMTP", "UTL_MAIL", "UTL_INADDR", "DBMS_LDAP"}
) # PRIV.NETPACKAGEPUBLIC
FILE_PACKAGES = frozenset(
{"UTL_FILE", "DBMS_LOB", "DBMS_ADVISOR"}
) # PRIV.FILESYSTEMPACKAGEPUBLIC
ENCRYPTION_PACKAGES = frozenset(
{"DBMS_CRYPTO", "DBMS_OBFUSCATION_TOOLKIT", "DBMS_RANDOM"}
) # PRIV.ENCRYPTPACKAGEPUBLIC
class Privileges(OracledbService):
"""Oracle Database privileges service.
Reads DBA_SYS_PRIVS and DBA_TAB_PRIVS to surface privileges granted to
the PUBLIC role, mirroring the privileges and roles findings of the
Oracle Database Security Assessment Tool (DBSAT).
"""
def __init__(self, provider: OracledbProvider):
super().__init__(__class__.__name__, provider)
self.public_system_privileges = self._list_public_system_privileges()
self.public_execute_packages = self._list_public_execute_packages()
def _list_public_system_privileges(self) -> list[str]:
"""List system privileges granted to PUBLIC (DBSAT PRIV.SYSPUBLIC)."""
logger.info("Privileges - Listing system privileges granted to PUBLIC...")
privileges = []
try:
rows = self._execute_query(
"SELECT privilege FROM dba_sys_privs WHERE grantee = 'PUBLIC'"
)
privileges = sorted(row[0] for row in rows)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return privileges
def _list_public_execute_packages(self) -> list[str]:
"""List SYS-owned packages executable by PUBLIC.
The checks intersect this list with the DBSAT package groups
(network, file system, encryption) defined above.
"""
logger.info("Privileges - Listing SYS packages executable by PUBLIC...")
packages = []
try:
rows = self._execute_query(
"SELECT table_name FROM dba_tab_privs "
"WHERE grantee = 'PUBLIC' AND privilege = 'EXECUTE' "
"AND owner = 'SYS'"
)
packages = sorted(row[0] for row in rows)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return packages
@@ -0,0 +1,4 @@
from prowler.providers.common.provider import Provider
from prowler.providers.oracledb.services.users.users_service import Users
users_client = Users(Provider.get_global_provider())
@@ -0,0 +1,37 @@
{
"Provider": "oracledb",
"CheckID": "users_default_tablespace_not_system",
"CheckTitle": "Application users do not use SYSTEM or SYSAUX as their default tablespace",
"CheckType": [],
"ServiceName": "users",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "OracleDatabaseUser",
"ResourceGroup": "database",
"Description": "Checks that non-Oracle-maintained database users do not have **SYSTEM** or **SYSAUX** as their default tablespace, keeping user data out of the tablespaces reserved for the data dictionary (DBSAT finding USER.TABLESPACE).",
"Risk": "User objects in SYSTEM or SYSAUX can exhaust space needed by the data dictionary, causing database-wide outages, and blur the boundary between application data and dictionary objects that stricter protections rely on.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/admin/managing-tablespaces.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "ALTER USER <username> DEFAULT TABLESPACE users;",
"NativeIaC": "",
"Other": "1. Create or choose an application tablespace\n2. Run ALTER USER <username> DEFAULT TABLESPACE <tablespace>\n3. Move any existing objects out of SYSTEM/SYSAUX with ALTER TABLE ... MOVE / ALTER INDEX ... REBUILD",
"Terraform": ""
},
"Recommendation": {
"Text": "Give every application user a dedicated default tablespace and move existing user objects out of SYSTEM and SYSAUX.",
"Url": "https://hub.prowler.com/check/users_default_tablespace_not_system"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,39 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.users.users_client import users_client
RESERVED_TABLESPACES = ("SYSTEM", "SYSAUX")
class users_default_tablespace_not_system(Check):
"""Check that application users do not default to SYSTEM or SYSAUX."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A list of reports, one per non-Oracle-maintained user.
"""
findings = []
for user in users_client.users:
if user.oracle_maintained:
continue
report = CheckReportOracledb(
metadata=self.metadata(),
resource=user,
resource_name=user.name,
resource_id=user.name,
)
if user.default_tablespace in RESERVED_TABLESPACES:
report.status = "FAIL"
report.status_extended = (
f"User {user.name} uses reserved tablespace "
f"{user.default_tablespace} as its default tablespace."
)
else:
report.status = "PASS"
report.status_extended = (
f"User {user.name} uses tablespace "
f"{user.default_tablespace} as its default tablespace."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "oracledb",
"CheckID": "users_failed_login_attempts_limited",
"CheckTitle": "Database users lock after a limited number of failed login attempts",
"CheckType": [],
"ServiceName": "users",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "OracleDatabaseUser",
"ResourceGroup": "IAM",
"Description": "Checks that each non-Oracle-maintained database user has a profile with a limited **FAILED_LOGIN_ATTEMPTS**, so accounts lock after repeated authentication failures (DBSAT finding USER.NOLOCK).",
"Risk": "Without an account lockout threshold, attackers can brute-force passwords online without limit. A single weak password then exposes the account and every privilege granted to it.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/configuring-authentication.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "ALTER PROFILE <profile> LIMIT FAILED_LOGIN_ATTEMPTS 10;",
"NativeIaC": "",
"Other": "1. Identify the profile of each failing user in DBA_USERS\n2. Set a lockout threshold: ALTER PROFILE <profile> LIMIT FAILED_LOGIN_ATTEMPTS 10\n3. Optionally set PASSWORD_LOCK_TIME to control how long the account stays locked",
"Terraform": ""
},
"Recommendation": {
"Text": "Assign every user a profile with a finite FAILED_LOGIN_ATTEMPTS (Oracle's default profile uses 10) so brute-force attempts lock the account.",
"Url": "https://hub.prowler.com/check/users_failed_login_attempts_limited"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,31 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.users.users_client import users_client
class users_failed_login_attempts_limited(Check):
"""Check that database users lock after a limited number of failed login attempts."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A list of reports, one per non-Oracle-maintained user.
"""
findings = []
for user in users_client.users:
if user.oracle_maintained:
continue
report = CheckReportOracledb(
metadata=self.metadata(),
resource=user,
resource_name=user.name,
resource_id=user.name,
)
if user.failed_login_attempts == "UNLIMITED":
report.status = "FAIL"
report.status_extended = f"User {user.name} allows unlimited failed login attempts (profile {user.profile})."
else:
report.status = "PASS"
report.status_extended = f"User {user.name} locks after {user.failed_login_attempts} failed login attempts (profile {user.profile})."
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "oracledb",
"CheckID": "users_inactive_account_lock_configured",
"CheckTitle": "Database users are locked after a period of inactivity",
"CheckType": [],
"ServiceName": "users",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "OracleDatabaseUser",
"ResourceGroup": "IAM",
"Description": "Checks that each non-Oracle-maintained database user has a profile with a limited **INACTIVE_ACCOUNT_TIME**, so unused accounts are locked automatically (DBSAT finding USER.INACTIVE).",
"Risk": "Dormant accounts are rarely monitored, keep their old privileges and often belong to departed users. They are prime targets for takeover because misuse is unlikely to be noticed.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/configuring-authentication.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "ALTER PROFILE <profile> LIMIT INACTIVE_ACCOUNT_TIME 90;",
"NativeIaC": "",
"Other": "1. Identify the profile of each failing user in DBA_USERS\n2. Set an inactivity limit: ALTER PROFILE <profile> LIMIT INACTIVE_ACCOUNT_TIME 90\n3. Review and drop accounts that are no longer needed",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure INACTIVE_ACCOUNT_TIME in every profile so accounts that stay unused for the defined number of days are locked automatically.",
"Url": "https://hub.prowler.com/check/users_inactive_account_lock_configured"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,31 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.users.users_client import users_client
class users_inactive_account_lock_configured(Check):
"""Check that database users are locked after a period of inactivity."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A list of reports, one per non-Oracle-maintained user.
"""
findings = []
for user in users_client.users:
if user.oracle_maintained:
continue
report = CheckReportOracledb(
metadata=self.metadata(),
resource=user,
resource_name=user.name,
resource_id=user.name,
)
if user.inactive_account_time in (None, "UNLIMITED"):
report.status = "FAIL"
report.status_extended = f"User {user.name} is never locked for inactivity (profile {user.profile})."
else:
report.status = "PASS"
report.status_extended = f"User {user.name} is locked after {user.inactive_account_time} days of inactivity (profile {user.profile})."
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "oracledb",
"CheckID": "users_password_expiration_configured",
"CheckTitle": "Database users have a limited password lifetime",
"CheckType": [],
"ServiceName": "users",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "OracleDatabaseUser",
"ResourceGroup": "IAM",
"Description": "Checks that each non-Oracle-maintained database user has a profile with a limited **PASSWORD_LIFE_TIME**, so passwords expire and must be rotated (DBSAT finding USER.NOEXPIRE).",
"Risk": "Passwords that never expire stay valid indefinitely, so a leaked or cracked credential grants persistent access. Long-lived passwords also increase the window for offline cracking of stolen verifiers.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/configuring-authentication.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "ALTER PROFILE <profile> LIMIT PASSWORD_LIFE_TIME 90;",
"NativeIaC": "",
"Other": "1. Identify the profile of each failing user in DBA_USERS\n2. Set a finite password lifetime: ALTER PROFILE <profile> LIMIT PASSWORD_LIFE_TIME 90\n3. For service accounts, consider gradual password rollover (PASSWORD_ROLLOVER_TIME) to rotate without downtime",
"Terraform": ""
},
"Recommendation": {
"Text": "Assign every user a profile with a finite PASSWORD_LIFE_TIME so passwords are rotated periodically, and use gradual password rollover for application accounts.",
"Url": "https://hub.prowler.com/check/users_password_expiration_configured"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,31 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.users.users_client import users_client
class users_password_expiration_configured(Check):
"""Check that database users have a limited password lifetime."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A list of reports, one per non-Oracle-maintained user.
"""
findings = []
for user in users_client.users:
if user.oracle_maintained:
continue
report = CheckReportOracledb(
metadata=self.metadata(),
resource=user,
resource_name=user.name,
resource_id=user.name,
)
if user.password_life_time == "UNLIMITED":
report.status = "FAIL"
report.status_extended = f"User {user.name} has an unlimited password lifetime (profile {user.profile})."
else:
report.status = "PASS"
report.status_extended = f"User {user.name} has a password lifetime of {user.password_life_time} days (profile {user.profile})."
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "oracledb",
"CheckID": "users_password_verify_function_configured",
"CheckTitle": "Database users have a password complexity verification function",
"CheckType": [],
"ServiceName": "users",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "OracleDatabaseUser",
"ResourceGroup": "IAM",
"Description": "Checks that each non-Oracle-maintained database user has a profile with a **PASSWORD_VERIFY_FUNCTION**, enforcing password complexity rules at password change time (DBSAT finding USER.PASSWORDFUNCTION).",
"Risk": "Without a verification function users can choose trivial passwords (short, dictionary words, the username itself), making online guessing and offline cracking of stolen verifiers far easier.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/configuring-authentication.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "ALTER PROFILE <profile> LIMIT PASSWORD_VERIFY_FUNCTION ora12c_verify_function;",
"NativeIaC": "",
"Other": "1. Identify the profile of each failing user in DBA_USERS\n2. Assign a verification function: ALTER PROFILE <profile> LIMIT PASSWORD_VERIFY_FUNCTION ora12c_verify_function\n3. Use ora12c_stig_verify_function or a custom function for stricter policies",
"Terraform": ""
},
"Recommendation": {
"Text": "Assign a password verification function (for example ora12c_verify_function) to every profile so password complexity is enforced when passwords are set or changed.",
"Url": "https://hub.prowler.com/check/users_password_verify_function_configured"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,31 @@
from prowler.lib.check.models import Check, CheckReportOracledb
from prowler.providers.oracledb.services.users.users_client import users_client
class users_password_verify_function_configured(Check):
"""Check that database users have a password complexity verification function."""
def execute(self) -> list[CheckReportOracledb]:
"""Execute the check logic.
Returns:
A list of reports, one per non-Oracle-maintained user.
"""
findings = []
for user in users_client.users:
if user.oracle_maintained:
continue
report = CheckReportOracledb(
metadata=self.metadata(),
resource=user,
resource_name=user.name,
resource_id=user.name,
)
if user.password_verify_function in (None, "NULL"):
report.status = "FAIL"
report.status_extended = f"User {user.name} has no password verification function (profile {user.profile})."
else:
report.status = "PASS"
report.status_extended = f"User {user.name} uses the password verification function {user.password_verify_function} (profile {user.profile})."
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "oracledb",
"CheckID": "users_sample_schemas_removed",
"CheckTitle": "Oracle sample schemas are not installed in the database",
"CheckType": [],
"ServiceName": "users",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "OracleDatabase",
"ResourceGroup": "IAM",
"Description": "Checks that Oracle **sample schemas** (SCOTT, HR, OE, SH, PM, IX, BI...) are not present in the database. Sample schemas are demo content with well-known passwords and objects and must not exist in production databases (DBSAT finding USER.SAMPLE).",
"Risk": "Sample schemas use published default passwords and predictable objects, giving attackers an easy initial foothold. Once inside, they can enumerate the data dictionary and escalate through granted privileges.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.oracle.com/en/database/oracle/oracle-database/19/comsc/introduction-to-sample-schemas.html",
"https://docs.oracle.com/en/database/oracle/security-assessment-tool/3.0.0/satug/"
],
"Remediation": {
"Code": {
"CLI": "DROP USER SCOTT CASCADE;",
"NativeIaC": "",
"Other": "1. Connect as a DBA user\n2. Review the sample schemas reported by the check\n3. Run DROP USER <schema> CASCADE for each sample schema after confirming it holds no business data",
"Terraform": ""
},
"Recommendation": {
"Text": "Remove Oracle sample schemas from production databases. If a schema must stay temporarily, lock and expire the account and remove it as soon as possible.",
"Url": "https://hub.prowler.com/check/users_sample_schemas_removed"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}

Some files were not shown because too many files have changed in this diff Show More