mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-19 17:40:25 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93935b2cff |
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
+38
@@ -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": ""
|
||||
}
|
||||
+34
@@ -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
|
||||
+38
@@ -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())
|
||||
+38
@@ -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": ""
|
||||
}
|
||||
+36
@@ -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
|
||||
+37
@@ -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": ""
|
||||
}
|
||||
+36
@@ -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
|
||||
+38
@@ -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": ""
|
||||
}
|
||||
+34
@@ -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
|
||||
+38
@@ -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": ""
|
||||
}
|
||||
+34
@@ -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
|
||||
+37
@@ -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": ""
|
||||
}
|
||||
+37
@@ -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
|
||||
+37
@@ -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": ""
|
||||
}
|
||||
+34
@@ -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
|
||||
+37
@@ -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": ""
|
||||
}
|
||||
+37
@@ -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
|
||||
+37
@@ -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": ""
|
||||
}
|
||||
+46
@@ -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())
|
||||
+38
@@ -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": ""
|
||||
}
|
||||
+43
@@ -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
|
||||
+37
@@ -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": ""
|
||||
}
|
||||
+43
@@ -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
|
||||
+38
@@ -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": ""
|
||||
}
|
||||
+43
@@ -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
|
||||
+38
@@ -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": ""
|
||||
}
|
||||
+37
@@ -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())
|
||||
+37
@@ -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": ""
|
||||
}
|
||||
+39
@@ -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
|
||||
+37
@@ -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": ""
|
||||
}
|
||||
+31
@@ -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
|
||||
+37
@@ -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": ""
|
||||
}
|
||||
+31
@@ -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
|
||||
+37
@@ -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": ""
|
||||
}
|
||||
+31
@@ -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
|
||||
+37
@@ -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": ""
|
||||
}
|
||||
+31
@@ -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
|
||||
+37
@@ -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
Reference in New Issue
Block a user