diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index ab40743b36..2033dcc420 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to the **Prowler API** are documented in this file. - Opt-in automatic recovery of allowlisted idempotent background tasks whose worker died during a deploy or crash: when enabled via `DJANGO_TASK_RECOVERY_ENABLED` (off by default), stuck summary and deletion tasks are detected and re-run instead of staying pending forever (scan and Jira tasks are excluded), with a `reconcile_orphan_tasks` management command for on-demand recovery [(#11416)](https://github.com/prowler-cloud/prowler/pull/11416) - DORA compliance framework support [(#11131)](https://github.com/prowler-cloud/prowler/pull/11131) - Label Postgres connections with `application_name=":"` (component injected per process via `DJANGO_APP_COMPONENT`) so connections are attributable by component in `pg_stat_activity` [(#11494)](https://github.com/prowler-cloud/prowler/pull/11494) +- DISA Okta IDaaS STIG V1R2 compliance framework export support for the Okta provider [(#11428)](https://github.com/prowler-cloud/prowler/pull/11428) ### 🔄 Changed diff --git a/api/src/backend/tasks/jobs/export.py b/api/src/backend/tasks/jobs/export.py index 185acb7f99..96bd03ee6c 100644 --- a/api/src/backend/tasks/jobs/export.py +++ b/api/src/backend/tasks/jobs/export.py @@ -58,6 +58,9 @@ from prowler.lib.outputs.compliance.mitre_attack.mitre_attack_azure import ( AzureMitreAttack, ) from prowler.lib.outputs.compliance.mitre_attack.mitre_attack_gcp import GCPMitreAttack +from prowler.lib.outputs.compliance.okta_idaas_stig.okta_idaas_stig_okta import ( + OktaIDaaSSTIG, +) from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_alibaba import ( ProwlerThreatScoreAlibaba, ) @@ -152,6 +155,9 @@ COMPLIANCE_CLASS_MAP = { ProwlerThreatScoreAlibaba, ), ], + "okta": [ + (lambda name: name.startswith("okta_idaas_stig"), OktaIDaaSSTIG), + ], } diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index dcb250d316..c9542d8345 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🚀 Added +- DISA Okta IDaaS STIG V1R2 compliance framework for the Okta provider, with a dedicated CSV output formatter and terminal summary table [(#11428)](https://github.com/prowler-cloud/prowler/pull/11428) - `sagemaker_models_monitor_enabled` check for AWS provider, verifying that each SageMaker monitoring schedule is in the `Scheduled` state so data and model drift is actively detected [(#11278)](https://github.com/prowler-cloud/prowler/pull/11278) - DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) universal compliance framework with AWS provider coverage across the five DORA pillars [(#11131)](https://github.com/prowler-cloud/prowler/pull/11131) - Okta authenticator and password policy checks for STIG-aligned hardening requirements [(#11465)](https://github.com/prowler-cloud/prowler/pull/11465) diff --git a/prowler/__main__.py b/prowler/__main__.py index 6cbaf575d9..703a9e49a0 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -102,6 +102,9 @@ from prowler.lib.outputs.compliance.mitre_attack.mitre_attack_azure import ( AzureMitreAttack, ) from prowler.lib.outputs.compliance.mitre_attack.mitre_attack_gcp import GCPMitreAttack +from prowler.lib.outputs.compliance.okta_idaas_stig.okta_idaas_stig_okta import ( + OktaIDaaSSTIG, +) from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_alibaba import ( ProwlerThreatScoreAlibaba, ) @@ -1314,6 +1317,33 @@ def prowler(): ) generated_outputs["compliance"].append(generic_compliance) generic_compliance.batch_write_data_to_file() + elif provider == "okta": + for compliance_name in input_compliance_frameworks: + if compliance_name.startswith("okta_idaas_stig"): + # Generate Okta IDaaS STIG Finding Object + filename = ( + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" + ) + okta_idaas_stig = OktaIDaaSSTIG( + findings=finding_outputs, + compliance=bulk_compliance_frameworks[compliance_name], + file_path=filename, + ) + generated_outputs["compliance"].append(okta_idaas_stig) + okta_idaas_stig.batch_write_data_to_file() + else: + filename = ( + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" + ) + generic_compliance = GenericCompliance( + findings=finding_outputs, + compliance=bulk_compliance_frameworks[compliance_name], + file_path=filename, + ) + generated_outputs["compliance"].append(generic_compliance) + generic_compliance.batch_write_data_to_file() else: # Dynamic fallback: any external/custom provider try: diff --git a/prowler/compliance/okta/__init__.py b/prowler/compliance/okta/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/compliance/okta/okta_idaas_stig_v1r2_okta.json b/prowler/compliance/okta/okta_idaas_stig_v1r2_okta.json new file mode 100644 index 0000000000..bac3d9132e --- /dev/null +++ b/prowler/compliance/okta/okta_idaas_stig_v1r2_okta.json @@ -0,0 +1,638 @@ +{ + "Framework": "Okta-IDaaS-STIG", + "Name": "DISA Okta Identity as a Service (IDaaS) STIG V1R2", + "Version": "1R2", + "Provider": "Okta", + "Description": "Defense Information Systems Agency (DISA) Security Technical Implementation Guide (STIG) for Okta Identity as a Service (IDaaS), Version 1 Release 2 (Benchmark Date: 05 Jan 2026).", + "Requirements": [ + { + "Id": "OKTA-APP-000020", + "Name": "Okta must log out a session after a 15-minute period of inactivity.", + "Description": "A session timeout lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not log out because of the temporary nature of the absence. Rather than relying on the user to manually lock their application session prior to vacating the vicinity, applications must be able to identify when a user's application session has idled and take action to initiate the session lock. The session lock is implemented at the point where session activity can be determined and/or controlled. This is typically at the operating system level and results in a system lock. However, it may be at the application level where the application interface window is secured instead. Satisfies: SRG-APP-000003, SRG-APP-000190", + "Checks": [ + "signon_global_session_idle_timeout_15min" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273186r1098825_rule", + "StigID": "OKTA-APP-000020", + "CCI": [ + "CCI-000057", + "CCI-001133" + ], + "CheckText": "From the Admin Console: 1. Select Security >> Global Session Policy. 2. In the Default Policy, verify a rule is configured at Priority 1 that is not named \"Default Rule\". 3. Click the edit icon next to the Priority 1 rule. 4. Verify the \"Maximum Okta global session idle time\" is set to 15 minutes. If \"Maximum Okta global session idle time\" is not set to 15 minutes, this is a finding.", + "FixText": "From the Admin Console: 1. Go to Security >> Global Session Policy. 2. Select the Default Policy. 3. In the Rules table, make these updates: - Click \"Add rule\". - Set \"Maximum Okta global session idle time\" to 15 minutes." + } + ] + }, + { + "Id": "OKTA-APP-000025", + "Name": "The Okta Admin Console must log out a session after a 15-minute period of inactivity.", + "Description": "A session timeout lock is a temporary action taken when a user stops work and moves away from the immediate physical vicinity of the information system but does not log out because of the temporary nature of the absence. Rather than relying on the user to manually lock their application session prior to vacating the vicinity, applications must be able to identify when a user's application session has idled and take action to initiate the session lock. The session lock is implemented at the point where session activity can be determined and/or controlled. This is typically at the operating system level and results in a system lock. However, it may be at the application level where the application interface window is secured instead.", + "Checks": [ + "application_admin_console_session_idle_timeout_15min" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273187r1098828_rule", + "StigID": "OKTA-APP-000025", + "CCI": [ + "CCI-000057" + ], + "CheckText": "From the Admin Console: 1. Select Applications >> Applications >> Okta Admin Console. 2. In the Sign On tab, under \"Okta Admin Console session\", verify the \"Maximum app session idle time\" is set to 15 minutes. If the \"Maximum app session idle time\" is not set to 15 minutes, this is a finding.", + "FixText": "From the Admin Console: 1. Select Applications >> Applications >> Okta Admin Console. 2. In the Sign On tab, under \"Okta Admin Console session\", set the \"Maximum app session idle time\" to 15 minutes." + } + ] + }, + { + "Id": "OKTA-APP-000090", + "Name": "Okta must automatically disable accounts after a 35-day period of account inactivity.", + "Description": "Attackers that are able to exploit an inactive account can potentially obtain and maintain undetected access to an application. Owners of inactive accounts will not notice if unauthorized access to their user account has been obtained. Applications must track periods of user inactivity and disable accounts after 35 days of inactivity. Such a process greatly reduces the risk that accounts will be hijacked, leading to a data compromise. To address access requirements, many application developers choose to integrate their applications with enterprise-level authentication/access mechanisms that meet or exceed access control policy requirements. Such integration allows the application developer to off-load those access control functions and focus on core application features and functionality. This policy does not apply to emergency accounts or infrequently used accounts. Infrequently used accounts are local login administrator accounts used by system administrators when network or normal login/access is not available. Emergency accounts are administrator accounts created in response to crisis situations. Satisfies: SRG-APP-000025, SRG-APP-000163, SRG-APP-000700", + "Checks": [ + "user_inactivity_automation_35d_enabled" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273188r1098831_rule", + "StigID": "OKTA-APP-000090", + "CCI": [ + "CCI-000017", + "CCI-000795", + "CCI-003627" + ], + "CheckText": "If Okta Services rely on external directory services for user sourcing, this is not applicable, and the connected directory services must perform this function. Go to Workflows >> Automations and verify that an Automation has been created to disable accounts after 35 days of inactivity. If the Okta configuration does not automatically disable accounts after a 35-day period of account inactivity, this is a finding.", + "FixText": "From the Admin Console: 1. Go to Workflow >> Automations and select \"Add Automation\". 2. Create a name for the Automation (e.g., \"User Inactivity\"). 3. Click \"Add Condition\" and select \"User Inactivity in Okta\". 4. In the duration field, enter 35 days and click \"Save\". 5 Click the edit button next to \"Select Schedule\". 6. Configure the \"Schedule\" field for \"Run Daily\" and set the \"Time\" field to an organizationally defined time to run this automation. Click \"Save\". 7. Click the edit button next to \"Select group membership\". 8. In the \"Applies to\" field, select the group \"Everyone\" by typing it into the field. Click \"Save\". 9. Click \"Add Action\" and select \"Change User lifecycle state in Okta\". 10. In the \"Change user state to\" field, select \"Suspended\" and click \"Save\". 11. Click the \"Inactive\" button near the top of the section screen and select \"Activate\"." + } + ] + }, + { + "Id": "OKTA-APP-000170", + "Name": "Okta must enforce the limit of three consecutive invalid login attempts by a user during a 15-minute time period.", + "Description": "By limiting the number of failed login attempts, the risk of unauthorized system access via user password guessing, otherwise known as brute forcing, is reduced. Limits are imposed by locking the account. Satisfies: SRG-APP-000065, SRG-APP-000345", + "Checks": [ + "authenticator_password_lockout_threshold_3" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273189r1098834_rule", + "StigID": "OKTA-APP-000170", + "CCI": [ + "CCI-000044", + "CCI-002238" + ], + "CheckText": "If Okta Services rely on external directory services for user sourcing, this check is not applicable, and the connected directory services must perform this function. From the Admin Console: 1. Go to Security >> Authenticators. 2. Click the \"Actions\" button next to \"Password\" and select \"Edit\". 3. For each Password Policy, verify the \"Lock Out\" section has the following values: - \"Lock out after 3 unsuccessful attempts\" is checked. - The value is set to \"3\". If Okta Services are not configured to automatically lock user accounts after three consecutive invalid login attempts, this is a finding.", + "FixText": "From the Admin Console: 1. Go to Security >> Authenticators. 2. Click the \"Actions\" button next to \"Password\" and select \"Edit\". 3. For each Password Policy, ensure the \"Lock Out\" section has the following values: - \"Lock out after 3 unsuccessful attempts\" is checked. - The value is set to \"3\"." + } + ] + }, + { + "Id": "OKTA-APP-000180", + "Name": "The Okta Dashboard application must be configured to allow authentication only via non-phishable authenticators.", + "Description": "Requiring the use of non-phishable authenticators protects against brute force/password dictionary attacks. This provides a better level of security while removing the need to lock out accounts after three attempts in 15 minutes.", + "Checks": [ + "application_dashboard_phishing_resistant_authentication" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273190r1099763_rule", + "StigID": "OKTA-APP-000180", + "CCI": [ + "CCI-000044" + ], + "CheckText": "From the Admin Console: 1. Go to Security >> Authentication Policies. 2. Click the \"Okta Dashboard\" policy. 3. Click the \"Actions\" button next to the top rule and select \"Edit\". 4. In the \"Possession factor constraints are\" section, verify the \"Phishing resistant\" box is checked. This will ensure that only phishing-resistant factors are used to access the Okta Dashboard. If in the \"Possession factor constraints are\" section the \"Phishing resistant\" box is not checked, this is a finding.", + "FixText": "From the Admin Console: 1. Go to Security >> Authentication Policies. 2. Click the \"Okta Dashboard\" policy. 3. Click the \"Actions\" button next to the top rule and select \"Edit\". 4. In the \"Possession factor constraints are\" section, ensure the \"Phishing resistant\" box is checked." + } + ] + }, + { + "Id": "OKTA-APP-000190", + "Name": "The Okta Admin Console application must be configured to allow authentication only via non-phishable authenticators.", + "Description": "Requiring the use of non-phishable authenticators protects against brute force/password dictionary attacks. This provides a better level of security while removing the need to lock out accounts after three attempts in 15 minutes.", + "Checks": [ + "application_admin_console_phishing_resistant_authentication" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273191r1099764_rule", + "StigID": "OKTA-APP-000190", + "CCI": [ + "CCI-000044" + ], + "CheckText": "From the Admin Console: 1. Go to Security >> Authentication Policies. 2. Click the \"Okta Admin Console\" policy. 3. Click the \"Actions\" button next to the top rule and select \"Edit\". 4. In the \"Possession factor constraints are\" section, verify the \"Phishing resistant\" box is checked. This will ensure that only phishing-resistant factors are used to access the Okta Dashboard. If in the \"Possession factor constraints are\" section the \"Phishing resistant\" box is not checked, this is a finding.", + "FixText": "From the Admin Console: 1. Go to Security >> Authentication Policies. 2. Click the \"Okta Admin Console\" policy. 3. Click the \"Actions\" button next to the top rule and select \"Edit\". 4. In the \"Possession factor constraints are\" section, ensure the \"Phishing resistant\" box is checked." + } + ] + }, + { + "Id": "OKTA-APP-000200", + "Name": "Okta must display the Standard Mandatory DOD Notice and Consent Banner before granting access to the application.", + "Description": "Display of the DOD-approved use notification before granting access to the application ensures that privacy and security notification verbiage used is consistent with applicable federal laws, Executive Orders, directives, policies, regulations, standards, and guidance. System use notifications are required only for access via login interfaces with human users and are not required when such human interfaces do not exist. The banner must be formatted in accordance with DTM-08-060. Use the following verbiage for applications that can accommodate banners of 1300 characters: \"You are accessing a U.S. Government (USG) Information System (IS) that is provided for USG-authorized use only. By using this IS (which includes any device attached to this IS), you consent to the following conditions: -The USG routinely intercepts and monitors communications on this IS for purposes including, but not limited to, penetration testing, COMSEC monitoring, network operations and defense, personnel misconduct (PM), law enforcement (LE), and counterintelligence (CI) investigations. -At any time, the USG may inspect and seize data stored on this IS. -Communications using, or data stored on, this IS are not private, are subject to routine monitoring, interception, and search, and may be disclosed or used for any USG-authorized purpose. -This IS includes security measures (e.g., authentication and access controls) to protect USG interests--not for your personal benefit or privacy. -Notwithstanding the above, using this IS does not constitute consent to PM, LE or CI investigative searching or monitoring of the content of privileged communications, or work product, related to personal representation or services by attorneys, psychotherapists, or clergy, and their assistants. Such communications and work product are private and confidential. See User Agreement for details.\" Use the following verbiage for operating systems that have severe limitations on the number of characters that can be displayed in the banner: \"I've read & consent to terms in IS user agreem't.\" Satisfies: SRG-APP-000068, SRG-APP-000069, SRG-APP-000070", + "Checks": [ + "signon_dod_warning_banner_configured" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273192r1098843_rule", + "StigID": "OKTA-APP-000200", + "CCI": [ + "CCI-000048", + "CCI-000050", + "CCI-001384", + "CCI-001385", + "CCI-001386", + "CCI-001387", + "CCI-001388" + ], + "CheckText": "Attempt to log in to the Okta tenant and verify the DOD-approved warning banner is in place. If the required warning banner is not present and complete, this is a finding.", + "FixText": "Follow the supplemental instructions in the \"Okta DOD Warning Banner Configuration Guide\" provided with this STIG package." + } + ] + }, + { + "Id": "OKTA-APP-000560", + "Name": "The Okta Admin Console application must be configured to use multifactor authentication.", + "Description": "Without the use of multifactor authentication, the ease of access to privileged functions is greatly increased. Multifactor authentication requires using two or more factors to achieve authentication. Factors include: (i) something a user knows (e.g., password/PIN); (ii) something a user has (e.g., cryptographic identification device, token); or (iii) something a user is (e.g., biometric). A privileged account is defined as an information system account with authorizations of a privileged user. Network access is defined as access to an information system by a user (or a process acting on behalf of a user) communicating through a network (e.g., local area network, wide area network, or the internet). Satisfies: SRG-APP-000149, SRG-APP-000154", + "Checks": [ + "application_admin_console_mfa_required" + ], + "Attributes": [ + { + "Section": "CAT I (High)", + "Severity": "high", + "RuleID": "SV-273193r1098846_rule", + "StigID": "OKTA-APP-000560", + "CCI": [ + "CCI-000765", + "CCI-004046" + ], + "CheckText": "From the Admin Console: 1. Go to Security >> Authentication Policies. 2. Click the \"Okta Admin Console\" policy. 3. Click the \"Actions\" button next to the top rule and select \"Edit\". 4. In the \"User must authenticate with\" field, verify that either \"Password/IdP + Another factor\" or \"Any 2 factor types\" is selected. If either of these settings is incorrect, this is a finding.", + "FixText": "From the Admin Console: 1. Go to Security >> Authentication Policies. 2. Click the \"Okta Admin Console\" policy. 3. Click the \"Actions\" button next to the top rule and select \"Edit\". 4. In the \"User must authenticate with\" field, select either \"Password/IdP + Another factor\" or \"Any 2 factor types\"." + } + ] + }, + { + "Id": "OKTA-APP-000570", + "Name": "The Okta Dashboard application must be configured to use multifactor authentication.", + "Description": "To ensure accountability and prevent unauthenticated access, nonprivileged users must use multifactor authentication to prevent potential misuse and compromise of the system. Multifactor authentication uses two or more factors to achieve authentication. Factors include: (i) Something you know (e.g., password/PIN); (ii) Something you have (e.g., cryptographic identification device, token); or (iii) Something you are (e.g., biometric). A nonprivileged account is any information system account with authorizations of a nonprivileged user. Network access is any access to an application by a user (or process acting on behalf of a user) where the access is obtained through a network connection. Applications integrating with the DOD Active Directory and using the DOD CAC are examples of compliant multifactor authentication solutions. Satisfies: SRG-APP-000150, SRG-APP-000155", + "Checks": [ + "application_dashboard_mfa_required" + ], + "Attributes": [ + { + "Section": "CAT I (High)", + "Severity": "high", + "RuleID": "SV-273194r1098849_rule", + "StigID": "OKTA-APP-000570", + "CCI": [ + "CCI-000766", + "CCI-004046" + ], + "CheckText": "From the Admin Console: 1. Go to Security >> Authentication Policies. 2. Click the \"Okta Dashboard\" policy. 3. Click the \"Actions\" button next to the top rule and select \"Edit\". 4. In the \"User must authenticate with\" field, verify that either \"Password/IdP + Another factor\" or \"Any 2 factor types\" is selected. If either of these settings is incorrect, this is a finding.", + "FixText": "From the Admin Console: 1. Go to Security >> Authentication Policies. 2. Click the \"Okta Dashboard\" policy. 3. Click the \"Actions\" button next to the top rule and select \"Edit\". 4. In the \"User must authenticate with\" field, select either \"Password/IdP + Another factor\" or \"Any 2 factor types\"." + } + ] + }, + { + "Id": "OKTA-APP-000650", + "Name": "Okta must enforce a minimum 15-character password length.", + "Description": "Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password length is one factor of several that helps to determine strength and how long it takes to crack a password. The shorter the password, the lower the number of possible combinations that need to be tested before the password is compromised. Use of more characters in a password helps to exponentially increase the time and/or resources required to compromise the password.", + "Checks": [ + "authenticator_password_minimum_length_15" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273195r1098852_rule", + "StigID": "OKTA-APP-000650", + "CCI": [ + "CCI-004066" + ], + "CheckText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password\" row and select \"Edit\". 3. For each listed policy, verify the \"Minimum Length\" field is set to at least \"15\" characters. If any policy is not set to at least \"15\", this is a finding.", + "FixText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password\" row and select \"Edit\". 3. For each listed policy: - Click \"Edit\". - Set the \"Minimum Length\" field to at least \"15\" characters." + } + ] + }, + { + "Id": "OKTA-APP-000670", + "Name": "Okta must enforce password complexity by requiring that at least one uppercase character be used.", + "Description": "Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determine how long it takes to crack a password. The more complex the password is, the greater the number of possible combinations that need to be tested before the password is compromised.", + "Checks": [ + "authenticator_password_complexity_uppercase" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273196r1098855_rule", + "StigID": "OKTA-APP-000670", + "CCI": [ + "CCI-004066" + ], + "CheckText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password\" row and select \"Edit\". 3. For each listed policy, verify \"Upper case letter\" is checked. For each policy, if \"Upper case letter\" is not checked, this is a finding.", + "FixText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password\" row and select \"Edit\". 3. For each listed policy: - Click \"Edit\". - Set \"Upper case letter\" to checked." + } + ] + }, + { + "Id": "OKTA-APP-000680", + "Name": "Okta must enforce password complexity by requiring that at least one lowercase character be used.", + "Description": "Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determine how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised.", + "Checks": [ + "authenticator_password_complexity_lowercase" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273197r1098858_rule", + "StigID": "OKTA-APP-000680", + "CCI": [ + "CCI-004066" + ], + "CheckText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password\" row and select \"Edit\". 3. For each listed policy, verify \"Lower case letter\" is checked. For each policy, if \"Lower case letter\" is not checked, this is a finding.", + "FixText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password\" row and select \"Edit\". 3. For each listed policy: - Click \"Edit\". - Set \"Lower case letter\" to checked." + } + ] + }, + { + "Id": "OKTA-APP-000690", + "Name": "Okta must enforce password complexity by requiring that at least one numeric character be used.", + "Description": "Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor of several that determine how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised.", + "Checks": [ + "authenticator_password_complexity_number" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273198r1098861_rule", + "StigID": "OKTA-APP-000690", + "CCI": [ + "CCI-004066" + ], + "CheckText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password\" row and select \"Edit\". 3. For each listed policy, verify \"Number (0-9)\" is checked. For each policy, if \"Number (0-9)\" is not checked, this is a finding.", + "FixText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password\" row and select \"Edit\". 3. For each listed policy: - Click \"Edit\". - Set \"Number (0-9)\" to checked." + } + ] + }, + { + "Id": "OKTA-APP-000700", + "Name": "Okta must enforce password complexity by requiring that at least one special character be used.", + "Description": "Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password complexity is one factor in determining how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. Special characters are not alphanumeric. Examples include: ~ ! @ # $ % ^ *.", + "Checks": [ + "authenticator_password_complexity_symbol" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273199r1098864_rule", + "StigID": "OKTA-APP-000700", + "CCI": [ + "CCI-004066" + ], + "CheckText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password\" row and select \"Edit\". 3. For each listed policy, verify \"Symbol (e.g., !@#$%^&*)\" is checked. For each policy, if \"Symbol (e.g., !@#$%^&*)\" is not checked, this is a finding.", + "FixText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password\" row and select \"Edit\". 3. For each listed policy: - Click \"Edit\". - Set \"Symbol (e.g., !@#$%^&*)\" to checked." + } + ] + }, + { + "Id": "OKTA-APP-000740", + "Name": "Okta must enforce 24 hours/one day as the minimum password lifetime.", + "Description": "Enforcing a minimum password lifetime helps prevent repeated password changes to defeat the password reuse or history enforcement requirement. Restricting this setting limits the user's ability to change their password. Passwords must be changed at specific policy-based intervals; however, if the application allows the user to immediately and continually change their password, it could be changed repeatedly in a short period of time to defeat the organization's policy regarding password reuse. Satisfies: SRG-APP-000173, SRG-APP-000870", + "Checks": [ + "authenticator_password_minimum_age_24h" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273200r1098867_rule", + "StigID": "OKTA-APP-000740", + "CCI": [ + "CCI-004066" + ], + "CheckText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password\" row and select \"Edit\". 3. For each listed policy, verify \"Minimum password age is XX hours\" is set to at least \"24\". For each policy, if \"Minimum password age is XX hours\" is not set to at least \"24\", this is a finding.", + "FixText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password\" row and select \"Edit\". 3. For each listed policy: - Click \"Edit\". - Set \"Minimum password age is XX hours\" to at least \"24\"." + } + ] + }, + { + "Id": "OKTA-APP-000745", + "Name": "Okta must enforce a 60-day maximum password lifetime restriction.", + "Description": "Any password, no matter how complex, can eventually be cracked. Therefore, passwords must be changed at specific intervals. One method of minimizing this risk is to use complex passwords and periodically change them. If the application does not limit the lifetime of passwords and force users to change their passwords, there is the risk that the system and/or application passwords could be compromised. This requirement does not include emergency administration accounts, which are meant for access to the application in case of failure. These accounts are not required to have maximum password lifetime restrictions.", + "Checks": [ + "authenticator_password_maximum_age_60d" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273201r1098870_rule", + "StigID": "OKTA-APP-000745", + "CCI": [ + "CCI-004066" + ], + "CheckText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password\" row and select \"Edit\". 3. For each listed policy, verify \"Password expires after XX days\" is set to \"60\". For each policy, if \"Password expires after XX days\" is not set to \"60\", this is a finding.", + "FixText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password\" row and select \"Edit\". 3. For each listed policy: - Click \"Edit\". - Set \"Password expires after XX days\" to \"60\"." + } + ] + }, + { + "Id": "OKTA-APP-001430", + "Name": "Okta must off-load audit records onto a central log server.", + "Description": "Information stored in one location is vulnerable to accidental or incidental deletion or alteration. Off-loading is a common process in information systems with limited audit storage capacity. Satisfies: SRG-APP-000358, SRG-APP-000080, SRG-APP-000125", + "Checks": [ + "systemlog_streaming_enabled" + ], + "Attributes": [ + { + "Section": "CAT I (High)", + "Severity": "high", + "RuleID": "SV-273202r1099766_rule", + "StigID": "OKTA-APP-001430", + "CCI": [ + "CCI-001851", + "CCI-000166", + "CCI-001348" + ], + "CheckText": "From the Admin Console: 1. Go to Reports >> Log Streaming. 2. Verify that a Log Stream connection is configured and active. Alternately, interview the information system security manager (ISSM) and verify that an external Security Information and Event Management (SIEM) system is pulling Okta logs via an Application Programming Interface (API). If either of these is not configured, this is a finding.", + "FixText": "From the Admin Console: 1. Go to Reports >> Log Streaming. 2. Select either \"AWS EventBridge\" or \"Splunk Cloud\" and click \"Next\". 3. Complete the necessary fields and click \"Save\". If Log Streaming is not an option because the SIEM required is not an option, customers can use the Okta Log API to export system logs in real time." + } + ] + }, + { + "Id": "OKTA-APP-001665", + "Name": "Okta must be configured to limit the global session lifetime to 18 hours.", + "Description": "Without reauthentication, users may access resources or perform tasks for which they do not have authorization. When applications provide the capability to change security roles or escalate the functional capability of the application, it is critical the user reauthenticate. In addition to the reauthentication requirements associated with session locks, organizations may require reauthentication of individuals and/or devices in other situations, including (but not limited to) the following circumstances. (i) When authenticators change; (ii) When roles change; (iii) When security categories of information systems change; (iv) When the execution of privileged functions occurs; (v) After a fixed period of time; or (vi) Periodically. Within the DOD, the minimum circumstances requiring reauthentication are privilege escalation and role changes.", + "Checks": [ + "signon_global_session_lifetime_18h" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273203r1099958_rule", + "StigID": "OKTA-APP-001665", + "CCI": [ + "CCI-002038" + ], + "CheckText": "From the Admin Console: 1. Select Security >> Global Session Policy. 2. In the Default Policy, verify a rule is configured at Priority 1 that is not named \"Default Rule\". 3. Click the \"Edit\" icon next to the Priority 1 rule. 4. Verify \"Maximum Okta global session lifetime\" is set to 18 hours. If the above is not set, this is a finding.", + "FixText": "From the Admin Console: 1. Go to Security >> Global Session Policy. 2. Select the Default Policy. 3. In the Rules table, make these updates: - Click \"Add rule\". - Set \"Maximum Okta global session lifetime\" to 18 hours." + } + ] + }, + { + "Id": "OKTA-APP-001670", + "Name": "Okta must be configured to accept Personal Identity Verification (PIV) credentials.", + "Description": "The use of PIV credentials facilitates standardization and reduces the risk of unauthorized access. DOD has mandated the use of the common access card (CAC) to support identity management and personal authentication for systems covered under HSPD 12, as well as a primary component of layered protection for national security systems. Satisfies: SRG-APP-000391, SRG-APP-000402, SRG-APP-000403", + "Checks": [ + "authenticator_smart_card_active" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273204r1098879_rule", + "StigID": "OKTA-APP-001670", + "CCI": [ + "CCI-001953", + "CCI-002009", + "CCI-002010" + ], + "CheckText": "From the Admin Console: 1. Go to Security >> Authenticators. 2. Verify that \"Smart Card Authenticator\" is listed and has \"Status\" listed as \"Active\". If \"Smart Card Authenticator\" is not listed or is not listed as \"Active\", this is a finding.", + "FixText": "From the Admin Console: 1. Go to Security >> Authenticators. 2. In the \"Setup\" tab, click \"Add authenticator\". 3. Select the configured Smart Card Identity Provider and finish configuration." + } + ] + }, + { + "Id": "OKTA-APP-001700", + "Name": "The Okta Verify application must be configured to connect only to FIPS-compliant devices.", + "Description": "Without device-to-device authentication, communications with malicious devices may be established. Bidirectional authentication provides stronger safeguards to validate the identity of other devices for connections that are of greater risk. Currently, DOD requires the use of AES for bidirectional authentication because it is the only FIPS-validated AES cipher block algorithm. For distributed architectures (e.g., service-oriented architectures), the decisions regarding the validation of authentication claims may be made by services separate from the services acting on those decisions. In such situations, it is necessary to provide authentication decisions (as opposed to the actual authenticators) to the services that need to act on those decisions. A local connection is any connection with a device communicating without the use of a network. A network connection is any connection with a device that communicates through a network (e.g., local area or wide area network; the internet). A remote connection is any connection with a device communicating through an external network (e.g., the internet). Because of the challenges of applying this requirement on a large scale, organizations are encouraged to apply the requirement only to those limited number (and type) of devices that truly need to support this capability.", + "Checks": [ + "authenticator_okta_verify_fips_compliant" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273205r1098882_rule", + "StigID": "OKTA-APP-001700", + "CCI": [ + "CCI-001967" + ], + "CheckText": "From the Admin Console: 1. Go to Security >> Authenticators. 2. From the \"Setup\" tab, select \"Edit Okta Verify\". 3. Review the \"FIPS Compliance\" field. If FIPS-compliant authentication is not enabled, this is a finding.", + "FixText": "From the Admin Console: 1. Go to Security >> Authenticators. 2. From the \"Setup\" tab, select \"Edit Okta Verify\". 3. In the \"FIPS Compliance\" field, choose whether users enrolling in Okta Verify can use FIPS-compliant devices only or any device. 4. Click \"Save\" after making any changes." + } + ] + }, + { + "Id": "OKTA-APP-001710", + "Name": "Okta must be configured to disable persistent global session cookies.", + "Description": "If cached authentication information is out of date, the validity of the authentication information may be questionable. Satisfies: SRG-APP-000400, SRG-APP-000157", + "Checks": [ + "signon_global_session_cookies_not_persistent" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273206r1098885_rule", + "StigID": "OKTA-APP-001710", + "CCI": [ + "CCI-002007", + "CCI-001942" + ], + "CheckText": "From the Admin Console: 1. Select Security >> Global Session Policy. 2. In the Default Policy, verify a rule is configured at Priority 1 that is not named \"Default Rule\". 3. Click the \"Edit\" icon next to the Priority 1 rule. 4. Verify \"Okta global session cookies persist across browser sessions\" is set to \"Disabled\". If the above it not set, this is a finding.", + "FixText": "From the Admin Console: 1. Go to Security >> Global Session Policy. 2. Select the Default Policy. 3. In the \"Rules\" table, make these updates: - Click \"Add rule\". - Set \"Okta global session cookies persist across browser sessions\" to Disable." + } + ] + }, + { + "Id": "OKTA-APP-001920", + "Name": "Okta must be configured to use only DOD-approved certificate authorities.", + "Description": "Untrusted Certificate Authorities (CA) can issue certificates, but they may be issued by organizations or individuals that seek to compromise DOD systems or by organizations with insufficient security controls. If the CA used for verifying the certificate is not DOD approved, trust of this CA has not been established. The DOD will accept only PKI certificates obtained from a DOD-approved internal or external CA. Reliance on CAs for the establishment of secure sessions includes, for example, the use of Transport Layer Security (TLS) certificates. This requirement focuses on communications protection for the application session rather than for the network packet. This requirement applies to applications that use communications sessions. This includes, but is not limited to, web-based applications and Service-Oriented Architectures (SOA). Satisfies: SRG-APP-000427, SRG-APP-000910", + "Checks": [ + "idp_smart_card_dod_approved_ca" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273207r1098888_rule", + "StigID": "OKTA-APP-001920", + "CCI": [ + "CCI-002470", + "CCI-004909" + ], + "CheckText": "From the Admin Console: 1. Select Security >> Identity Providers (IdPs). 2. Review the list of IdPs with \"Type\" as \"Smart Card\". If the IdP is not listed as \"Active\", this is a finding. 3. Select Actions >> Configure. 4. Under \"Certificate chain\", verify the certificate is from a DOD-approved CA. If the certificate is not from a DOD-approved CA, this is a finding.", + "FixText": "From the Admin Console: 1. Go to Security >> Identity Providers. 2. Click \"Add identity provider.\" 3. Click \"Smart Card IdP\". Click \"Next\". 4. Enter the name of the identity provider. 5. Build a certificate chain: - Click \"Browse\" to open a file explorer. Select the certificate file to add and click \"Open\". - To add another certificate, click \"Add Another\" and repeat step 1. - Click \"Build certificate chain\". On success, the chain and its certificates are shown. If the build failed, correct any issues and try again. - Click \"Reset certificate chain\" if replacing the current chain with a new one. 6. In \"IdP username\", select the \"idpuser.subjectAltNameUpn\" attribute. This is the attribute that stores the Electronic Data Interchange Personnel Identifier (EDIPI) on the CAC. 7. In the \"Match Against\" field, select the Okta Profile Attribute in which the EDIPI is to be stored." + } + ] + }, + { + "Id": "OKTA-APP-002980", + "Name": "Okta must validate passwords against a list of commonly used, expected, or compromised passwords.", + "Description": "Password-based authentication applies to passwords regardless of whether they are used in single-factor or multifactor authentication. Long passwords or passphrases are preferable over shorter passwords. Enforced composition rules provide marginal security benefits while decreasing usability. However, organizations may choose to establish certain rules for password generation (e.g., minimum character length for long passwords) under certain circumstances and can enforce this requirement in IA-5(1)(h). Account recovery can occur, for example, in situations when a password is forgotten. Cryptographically protected passwords include salted one-way cryptographic hashes of passwords. The list of commonly used, compromised, or expected passwords includes passwords obtained from previous breach corpuses, dictionary words, and repetitive or sequential characters. The list includes context-specific words, such as the name of the service, username, and derivatives thereof.", + "Checks": [ + "authenticator_password_common_password_check" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273208r1099769_rule", + "StigID": "OKTA-APP-002980", + "CCI": [ + "CCI-004058" + ], + "CheckText": "From the Admin Console: 1. Navigate to Security >> Authenticators. 2. Click the \"Actions\" button next to the Password authenticator and select \"Edit\". 3. Under the \"Password Settings\" section, verify the \"Common Password Check\" box is checked. If \"Common Password Check\" is not selected, this is a finding.", + "FixText": "From the Admin Console: 1. Navigate to Security >> Authenticators. 2. Click the \"Actions\" button next to the Password authenticator and select \"Edit\". 3. Under the \"Password Settings\" section, check the \"Common Password Check\" box." + } + ] + }, + { + "Id": "OKTA-APP-003010", + "Name": "Okta must prohibit password reuse for a minimum of five generations.", + "Description": "Password-based authentication applies to passwords regardless of whether they are used in single-factor or multifactor authentication. Long passwords or passphrases are preferable over shorter passwords. Enforced composition rules provide marginal security benefits while decreasing usability. However, organizations may choose to establish certain rules for password generation (e.g., minimum character length for long passwords) under certain circumstances and can enforce this requirement in IA-5(1)(h). Account recovery can occur, for example, in situations when a password is forgotten. Cryptographically protected passwords include salted one-way cryptographic hashes of passwords. The list of commonly used, compromised, or expected passwords includes passwords obtained from previous breach corpuses, dictionary words, and repetitive or sequential characters. The list includes context-specific words, such as the name of the service, username, and derivatives thereof.", + "Checks": [ + "authenticator_password_history_5" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-273209r1098894_rule", + "StigID": "OKTA-APP-003010", + "CCI": [ + "CCI-004061" + ], + "CheckText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password row\" and select \"Edit\". 3. For each listed policy, verify \"Enforce password history for last XX passwords\" is set to \"5\". If any policy is not set to at least \"5\", this is a finding.", + "FixText": "From the Admin Console: 1. Select Security >> Authenticators. 2. Click the \"Actions\" button next to the \"Password\" row and select \"Edit\". 3. For each listed policy: - Click \"Edit\". - Set \"Enforce password history for last XX passwords\" to \"5\"." + } + ] + }, + { + "Id": "OKTA-APP-003240", + "Name": "Okta API tokens must be configured with Network Zones to restrict authorization from known networks.", + "Description": "An access token is a piece of data that represents the authorization granted to a user or NPE to access specific systems or information resources. Access tokens enable controlled access to services and resources. Properly managing the lifecycle of access tokens, including their issuance, validation, and revocation, is crucial to maintaining confidentiality of data and systems. Restricting token validity to a specific audience, e.g., an application or security domain, and restricting token validity lifetimes are important practices. Access tokens are revoked or invalidated if they are compromised, lost, or are no longer needed to mitigate the risks associated with stolen or misused tokens. API tokens have the potential to be replicated or stolen (just like a password). Because of this, it is important to only allow API tokens to authenticate from known IP ranges as this limits an adversary's ability to use a token to gain access.", + "Checks": [ + "apitoken_restricted_to_network_zone" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-279689r1155066_rule", + "StigID": "OKTA-APP-003240", + "CCI": [ + "CCI-005165", + "CCI-000366" + ], + "CheckText": "From the Admin Console: 1. Select the \"Security\" menu, and then click the \"API\" item. 2. Click the \"Tokens\" tab. 3. For each token listed, click the token name link. 4. In the \"Security\" section, verify the \"Token can be used from\" setting is mapped to a known network zone for the application calling the API. If a network zone for each API access token is not defined, this is a finding.", + "FixText": "From the Admin Console: 1. Select the \"Security\" menu, and then click the \"API\" item. 2. Click the \"Tokens\" tab. 3. For each token listed, click the token name link. 4. In the \"Security\" section, click \"Edit\". 5. Set the \"Token can be used from\" setting to the known network zone for the application calling the API. 6. Click \"Save\"." + } + ] + }, + { + "Id": "OKTA-APP-003241", + "Name": "Okta API tokens must be created under new dedicated user accounts.", + "Description": "An access token is a piece of data that represents the authorization granted to a user or NPE to access specific systems or information resources. Access tokens enable controlled access to services and resources. Properly managing the lifecycle of access tokens, including their issuance, validation, and revocation, is crucial to maintaining confidentiality of data and systems. Restricting token validity to a specific audience, e.g., an application or security domain, and restricting token validity lifetimes are important practices. Access tokens are revoked or invalidated if they are compromised, lost, or are no longer needed to mitigate the risks associated with stolen or misused tokens. When API tokens are created, they inherit the permissions of the user that created them. Therefore, API tokens should only be created from dedicated accounts and permissions must be constrained to least privilege for that dedicated user account and token. No API tokens should be created using a Super Admin account.", + "Checks": [ + "apitoken_not_super_admin" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-279690r1155069_rule", + "StigID": "OKTA-APP-003241", + "CCI": [ + "CCI-005165", + "CCI-000366" + ], + "CheckText": "From the Admin Console: 1. Select the \"Security\" menu, and then click the \"API\" item. 2. Click the \"Tokens\" tab. 3. For each token listed, verify that the Role listed is not \"Super Admin\", and that the account has been specifically created for that token. 4. Click the account name to be token to the user profile for that user. 5. Verify the user only has an administrator role (standard or customer) applied that is correctly scoped as required and documented in the Okta Access Control policy. If the token is using a Super Administrator account, or one that is not properly scoped per the Access Control policy, this is a finding. Note: If a Super Admin token is required for system operation, then this permanent finding.", + "FixText": "From the Admin Console: 1. Select the \"Security\" menu, and then click the \"API\" item. 2. Click the \"Tokens\" tab. 3. For each token listed that has \"Super Admin\" or an improperly scoped Admin account, delete the token and create a new one with the appropriately scoped permissions. 4. Verify the application performing the API calls with the new token has been updated." + } + ] + }, + { + "Id": "OKTA-APP-003242", + "Name": "The Okta Global Session policy must be configured to allow or deny IP based access in accordance with the Access Control policy for Okta.", + "Description": "To mitigate the risk of unauthorized access to sensitive information by entities that have been issued certificates by DOD-approved PKIs, all DOD systems (e.g., networks, web servers, and web portals) must be properly configured to incorporate access control methods that do not rely solely on the possession of a certificate for access. Successful authentication must not automatically give an entity access to an asset or security boundary. Authorization procedures and controls must be implemented to ensure each authenticated entity also has a validated and current authorization. Authorization is the process of determining whether an entity, once authenticated, is permitted to access a specific asset. Information systems use access control policies and enforcement mechanisms to implement this requirement. Access Control policies include identity-based policies, role-based policies, and attribute-based policies. Access enforcement mechanisms include access control lists, access control matrices, and cryptography. These policies and mechanisms must be employed by the application to control access between users (or processes acting on behalf of users) and objects (e.g., devices, files, records, processes, programs, and domains) in the information system. The Okta Global Session Policy is applied at the organization level and before any application-specific authentication policies are processed. The Okta authorization package should contain an access control policy that defines IP ranges from which to either allow or deny access. This list (either as an explicit allow or explicit deny) can be implemented in the Global Session Policy.", + "Checks": [ + "signon_global_session_policy_network_zone_enforced" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-279691r1155072_rule", + "StigID": "OKTA-APP-003242", + "CCI": [ + "CCI-000213" + ], + "CheckText": "From the Admin Console: 1. Select the \"Security\" menu, and then click the \"Global Session Policy\" item. 2. In the \"Policy Settings\" section, verify the \"IF User's IP is\" setting is correctly set to either allow or deny based on the organization defined policy. If the Okta Global Session Policy is not configured to restrict access to specific IP ranges, this is a finding.", + "FixText": "From the Admin Console: 1. Select the \"Security\" menu, and then click the \"Global Session Policy\" item. 2. In the Policy Settings section, configure the \"IF User's IP is\" setting to correctly set the appropriate network to either allow or deny based on the Access Control Policy." + } + ] + }, + { + "Id": "OKTA-APP-003243", + "Name": "Okta must be configured with Network Zones defined to block anonymized proxies according to organizationally defined policy.", + "Description": "A mechanism to detect and prevent unauthorized communication flow must be configured or provided as part of the system design. If information flow is not enforced based on approved authorizations, the system may become compromised. Information flow control regulates where information is allowed to travel within a system and between interconnected systems. The flow of all application information must be monitored and controlled so it does not introduce any unacceptable risk to the systems or data. Application-specific examples of enforcement occurs in systems that employ rule sets or establish configuration settings that restrict information system services, or provide a message filtering capability based on message content (e.g., implementing key word searches or using document characteristics). Applications providing information flow control must be able to enforce approved authorizations for controlling the flow of information between interconnected systems in accordance with applicable policy. Working with the organizational CSSP, the ISSM should obtain a list of known anonymizer proxies that exist on the commercial internet. If this is not available from the CSSP, then the Okta-provided \"Enhanced dynamic zone blocklist\" should be activated.", + "Checks": [ + "network_zone_block_anonymized_proxies" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-279692r1155075_rule", + "StigID": "OKTA-APP-003243", + "CCI": [ + "CCI-001414" + ], + "CheckText": "From the Admin Console: 1. Select the \"Security\" menu, and then click the \"Networks' item. 2. If the CSSP has provided a list of anonymizers to block, verify the \"IP Block list\" is configured with them. a. Click the pencil icon next to IP Block list. b. Verify the \"Gateway IPs\" section contains all of the IP ranges in the provided list. 3. If the CSSP is not able to provide a list, then implement the Okta managed list. a. Verify the \"Enhanced dynamic zone blocklist\" is set to \"Active\". If Network Zones are not configured to block anonymous proxies, this is a finding.", + "FixText": "From the Admin Console: 1. Select the \"Security\" menu, and then click the \"Networks\" item. 2. If the CSSP has provided a list of anonymizers to block, add the IP ranges to the \"IP Block list\". a. Click the pencil icon next to IP Block list. b. Add the IP ranges to the \"Gateway IPs\" section and click \"Save\". 3. If the CSSP is not able to provide a list, then implement the Okta managed list. a. Set the \"Enhanced dynamic zone blocklist\" to \"Active\"." + } + ] + }, + { + "Id": "OKTA-APP-003244", + "Name": "For each application integrated with Okta, network zones must be defined in its authentication policy.", + "Description": "A mechanism to detect and prevent unauthorized communication flow must be configured or provided as part of the system design. If information flow is not enforced based on approved authorizations, the system may become compromised. Information flow control regulates where information is allowed to travel within a system and between interconnected systems. The flow of all application information must be monitored and controlled so it does not introduce any unacceptable risk to the systems or data. Application-specific examples of enforcement occurs in systems that employ rule sets or establish configuration settings that restrict information system services, or provide a message filtering capability based on message content (e.g., implementing key word searches or using document characteristics). Applications providing information flow control must be able to enforce approved authorizations for controlling the flow of information between interconnected systems in accordance with applicable policy. Each application in Okta should have a well defined access control policy that takes into account the end user network. This should be documented in the Access Control policy for each application. As an example, access to an application may be restricted to a specific location by policy. In this case, a network defining that specific location should be created.", + "Checks": [ + "application_authentication_policy_network_zone_enforced" + ], + "Attributes": [ + { + "Section": "CAT II (Medium)", + "Severity": "medium", + "RuleID": "SV-279693r1155078_rule", + "StigID": "OKTA-APP-003244", + "CCI": [ + "CCI-001414" + ], + "CheckText": "For each application integrated into Okta: 1. From the Admin console, open the \"Security\" menu, and then select \"Networks\". 2. Verify the list of networks includes all necessary allow or block lists. If any application is not configured with network zones, this is a finding.", + "FixText": "For each application, starting at the admin console: 1. Open the \"Applications\" group from the Menu, and then click the \"Applications\" menu item. 2. Click the application name. 3. Click the \"Sign On\" tab. 4. Scroll to the \"User Authentication\" section, and then click \"Edit\". 5. Select the appropriate Authentication policy from the pull down, and then click \"Save\". 6. Click \"View Policy Details\". 7. For each nondefault rule: a. Select \"Edit\" from the Actions menu. b. In the \"IF\" section, verify the \"User is\" setting has the appropriate allow or deny range has been selected based on the Access Control policy for the application. c. Scroll down to the bottom and click \"Save\". 8. For the Catch-All rule: a. Select \"Edit\" from the Actions menu. b. Scroll down to the \"Then\" section. c. For the \"Access is\" setting, select \"Denied\", and then click \"Save\"." + } + ] + } + ] +} diff --git a/prowler/lib/check/compliance_models.py b/prowler/lib/check/compliance_models.py index 3610893008..f883bf60b1 100644 --- a/prowler/lib/check/compliance_models.py +++ b/prowler/lib/check/compliance_models.py @@ -283,6 +283,26 @@ class CSA_CCM_Requirement_Attribute(BaseModel): ScopeApplicability: list[dict] +class STIG_Requirement_Attribute_Severity(str, Enum): + """DISA STIG Requirement Attribute Severity (maps to CAT I/II/III)""" + + high = "high" + medium = "medium" + low = "low" + + +class STIG_Requirement_Attribute(BaseModel): + """DISA STIG Requirement Attribute""" + + Section: str + Severity: STIG_Requirement_Attribute_Severity + RuleID: str + StigID: str + CCI: Optional[list[str]] = None + CheckText: Optional[str] = None + FixText: Optional[str] = None + + # Base Compliance Model # TODO: move this to compliance folder class Compliance_Requirement(BaseModel): @@ -303,6 +323,7 @@ class Compliance_Requirement(BaseModel): CCC_Requirement_Attribute, C5Germany_Requirement_Attribute, CSA_CCM_Requirement_Attribute, + STIG_Requirement_Attribute, # Generic_Compliance_Requirement_Attribute must be the last one since it is the fallback for generic compliance framework Generic_Compliance_Requirement_Attribute, ] diff --git a/prowler/lib/outputs/compliance/compliance.py b/prowler/lib/outputs/compliance/compliance.py index 65ad8af0b3..4e4bd78232 100644 --- a/prowler/lib/outputs/compliance/compliance.py +++ b/prowler/lib/outputs/compliance/compliance.py @@ -18,6 +18,9 @@ from prowler.lib.outputs.compliance.kisa_ismsp.kisa_ismsp import get_kisa_ismsp_ from prowler.lib.outputs.compliance.mitre_attack.mitre_attack import ( get_mitre_attack_table, ) +from prowler.lib.outputs.compliance.okta_idaas_stig.okta_idaas_stig import ( + get_okta_idaas_stig_table, +) from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore import ( get_prowler_threatscore_table, ) @@ -252,6 +255,15 @@ def display_compliance_table( output_directory, compliance_overview, ) + elif compliance_framework.startswith("okta_idaas_stig"): + get_okta_idaas_stig_table( + findings, + bulk_checks_metadata, + compliance_framework, + output_filename, + output_directory, + compliance_overview, + ) else: # Try provider-specific table first, fall back to generic from prowler.providers.common.provider import Provider diff --git a/prowler/lib/outputs/compliance/okta_idaas_stig/__init__.py b/prowler/lib/outputs/compliance/okta_idaas_stig/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/lib/outputs/compliance/okta_idaas_stig/models.py b/prowler/lib/outputs/compliance/okta_idaas_stig/models.py new file mode 100644 index 0000000000..674d9656b7 --- /dev/null +++ b/prowler/lib/outputs/compliance/okta_idaas_stig/models.py @@ -0,0 +1,32 @@ +from typing import Optional + +from pydantic.v1 import BaseModel + + +class OktaIDaaSSTIGModel(BaseModel): + """ + OktaIDaaSSTIGModel generates a finding's output in DISA Okta IDaaS STIG Compliance format. + """ + + Provider: str + Description: str + OrganizationDomain: str + AssessmentDate: str + Requirements_Id: str + Requirements_Name: str + Requirements_Description: str + Requirements_Attributes_Section: str + Requirements_Attributes_Severity: str + Requirements_Attributes_RuleID: str + Requirements_Attributes_StigID: str + Requirements_Attributes_CCI: Optional[list[str]] = None + Requirements_Attributes_CheckText: Optional[str] = None + Requirements_Attributes_FixText: Optional[str] = None + Status: str + StatusExtended: str + ResourceId: str + ResourceName: str + CheckId: str + Muted: bool + Framework: str + Name: str diff --git a/prowler/lib/outputs/compliance/okta_idaas_stig/okta_idaas_stig.py b/prowler/lib/outputs/compliance/okta_idaas_stig/okta_idaas_stig.py new file mode 100644 index 0000000000..5c76055a06 --- /dev/null +++ b/prowler/lib/outputs/compliance/okta_idaas_stig/okta_idaas_stig.py @@ -0,0 +1,98 @@ +from colorama import Fore, Style +from tabulate import tabulate + +from prowler.config.config import orange_color + + +def get_okta_idaas_stig_table( + findings: list, + bulk_checks_metadata: dict, + compliance_framework: str, + output_filename: str, + output_directory: str, + compliance_overview: bool, +): + section_table = { + "Provider": [], + "Section": [], + "Status": [], + "Muted": [], + } + pass_count = [] + fail_count = [] + muted_count = [] + sections = {} + for index, finding in enumerate(findings): + check = bulk_checks_metadata[finding.check_metadata.CheckID] + check_compliances = check.Compliance + for compliance in check_compliances: + if compliance.Framework == "Okta-IDaaS-STIG": + for requirement in compliance.Requirements: + for attribute in requirement.Attributes: + section = attribute.Section + + if section not in sections: + sections[section] = {"FAIL": 0, "PASS": 0, "Muted": 0} + + if finding.muted: + if index not in muted_count: + muted_count.append(index) + sections[section]["Muted"] += 1 + else: + if finding.status == "FAIL" and index not in fail_count: + fail_count.append(index) + sections[section]["FAIL"] += 1 + elif finding.status == "PASS" and index not in pass_count: + pass_count.append(index) + sections[section]["PASS"] += 1 + + sections = dict(sorted(sections.items())) + for section in sections: + section_table["Provider"].append(compliance.Provider) + section_table["Section"].append(section) + if sections[section]["FAIL"] > 0: + section_table["Status"].append( + f"{Fore.RED}FAIL({sections[section]['FAIL']}){Style.RESET_ALL}" + ) + else: + if sections[section]["PASS"] > 0: + section_table["Status"].append( + f"{Fore.GREEN}PASS({sections[section]['PASS']}){Style.RESET_ALL}" + ) + else: + section_table["Status"].append(f"{Fore.GREEN}PASS{Style.RESET_ALL}") + section_table["Muted"].append( + f"{orange_color}{sections[section]['Muted']}{Style.RESET_ALL}" + ) + + if ( + len(fail_count) + len(pass_count) + len(muted_count) > 1 + ): # If there are no resources, don't print the compliance table + print( + f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:" + ) + total_findings_count = len(fail_count) + len(pass_count) + len(muted_count) + overview_table = [ + [ + f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}", + f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}", + f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}", + ] + ] + print(tabulate(overview_table, tablefmt="rounded_grid")) + if not compliance_overview: + if len(fail_count) > 0 and len(section_table["Section"]) > 0: + print( + f"\nFramework {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Results:" + ) + print( + tabulate( + section_table, + tablefmt="rounded_grid", + headers="keys", + ) + ) + print(f"\nDetailed results of {compliance_framework.upper()} are in:") + print( + f" - CSV: {output_directory}/compliance/{output_filename}_{compliance_framework}.csv\n" + ) diff --git a/prowler/lib/outputs/compliance/okta_idaas_stig/okta_idaas_stig_okta.py b/prowler/lib/outputs/compliance/okta_idaas_stig/okta_idaas_stig_okta.py new file mode 100644 index 0000000000..25f71b4def --- /dev/null +++ b/prowler/lib/outputs/compliance/okta_idaas_stig/okta_idaas_stig_okta.py @@ -0,0 +1,95 @@ +from prowler.config.config import timestamp +from prowler.lib.check.compliance_models import Compliance +from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput +from prowler.lib.outputs.compliance.okta_idaas_stig.models import OktaIDaaSSTIGModel +from prowler.lib.outputs.finding import Finding + + +class OktaIDaaSSTIG(ComplianceOutput): + """ + This class represents the Okta IDaaS STIG compliance output. + + Attributes: + - _data (list): A list to store transformed data from findings. + - _file_descriptor (TextIOWrapper): A file descriptor to write data to a file. + + Methods: + - transform: Transforms findings into Okta IDaaS STIG compliance format. + """ + + def transform( + self, + findings: list[Finding], + compliance: Compliance, + _compliance_name: str, + ) -> None: + """ + Transforms a list of findings into Okta IDaaS STIG compliance format. + + Parameters: + - findings (list): A list of findings. + - compliance (Compliance): A compliance model. + - _compliance_name (str): The name of the compliance model (unused). + + Returns: + - None + """ + for finding in findings: + for requirement in compliance.Requirements: + # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). + if finding.check_id in requirement.Checks: + for attribute in requirement.Attributes: + compliance_row = OktaIDaaSSTIGModel( + Provider=finding.provider, + Description=compliance.Description, + OrganizationDomain=finding.account_name, + AssessmentDate=str(timestamp), + Requirements_Id=requirement.Id, + Requirements_Name=requirement.Name, + Requirements_Description=requirement.Description, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_Severity=attribute.Severity.value, + Requirements_Attributes_RuleID=attribute.RuleID, + Requirements_Attributes_StigID=attribute.StigID, + Requirements_Attributes_CCI=attribute.CCI, + Requirements_Attributes_CheckText=attribute.CheckText, + Requirements_Attributes_FixText=attribute.FixText, + Status=finding.status, + StatusExtended=finding.status_extended, + ResourceId=finding.resource_uid, + ResourceName=finding.resource_name, + CheckId=finding.check_id, + Muted=finding.muted, + Framework=compliance.Framework, + Name=compliance.Name, + ) + self._data.append(compliance_row) + # Add manual requirements to the compliance output + for requirement in compliance.Requirements: + if not requirement.Checks: + for attribute in requirement.Attributes: + compliance_row = OktaIDaaSSTIGModel( + Provider=compliance.Provider.lower(), + Description=compliance.Description, + OrganizationDomain="", + AssessmentDate=str(timestamp), + Requirements_Id=requirement.Id, + Requirements_Name=requirement.Name, + Requirements_Description=requirement.Description, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_Severity=attribute.Severity.value, + Requirements_Attributes_RuleID=attribute.RuleID, + Requirements_Attributes_StigID=attribute.StigID, + Requirements_Attributes_CCI=attribute.CCI, + Requirements_Attributes_CheckText=attribute.CheckText, + Requirements_Attributes_FixText=attribute.FixText, + Status="MANUAL", + StatusExtended="Manual check", + ResourceId="manual_check", + ResourceName="Manual check", + CheckId="manual", + Muted=False, + Framework=compliance.Framework, + Name=compliance.Name, + ) + self._data.append(compliance_row) diff --git a/tests/lib/outputs/compliance/display_compliance_table_test.py b/tests/lib/outputs/compliance/display_compliance_table_test.py index a55789a7fd..bd854d6d7d 100644 --- a/tests/lib/outputs/compliance/display_compliance_table_test.py +++ b/tests/lib/outputs/compliance/display_compliance_table_test.py @@ -103,6 +103,15 @@ class TestDispatchStartswith: display_compliance_table(compliance_framework=framework_name, **_COMMON) mock_fn.assert_called_once() + @pytest.mark.parametrize( + "framework_name", + ["okta_idaas_stig_v1r2_okta"], + ) + @patch(f"{MODULE}.get_okta_idaas_stig_table") + def test_okta_idaas_stig_dispatch(self, mock_fn, framework_name): + display_compliance_table(compliance_framework=framework_name, **_COMMON) + mock_fn.assert_called_once() + @pytest.mark.parametrize( "framework_name", [ diff --git a/tests/lib/outputs/compliance/fixtures.py b/tests/lib/outputs/compliance/fixtures.py index f085460abd..a8fc7aa7e5 100644 --- a/tests/lib/outputs/compliance/fixtures.py +++ b/tests/lib/outputs/compliance/fixtures.py @@ -16,6 +16,7 @@ from prowler.lib.check.compliance_models import ( Mitre_Requirement_Attribute_Azure, Mitre_Requirement_Attribute_GCP, Prowler_ThreatScore_Requirement_Attribute, + STIG_Requirement_Attribute, ) CIS_1_4_AWS = Compliance( @@ -1258,3 +1259,47 @@ ASD_ESSENTIAL_EIGHT_AWS = Compliance( ), ], ) + +OKTA_IDAAS_STIG_OKTA = Compliance( + Framework="Okta-IDaaS-STIG", + Name="DISA Okta Identity as a Service (IDaaS) STIG V1R2", + Version="1R2", + Provider="Okta", + Description="Defense Information Systems Agency (DISA) Security Technical Implementation Guide (STIG) for Okta Identity as a Service (IDaaS).", + Requirements=[ + Compliance_Requirement( + Id="OKTA-APP-000020", + Name="Okta must log out a session after a 15-minute period of inactivity.", + Description="A session timeout lock is a temporary action taken when a user stops work and moves away from the immediate vicinity of the information system.", + Attributes=[ + STIG_Requirement_Attribute( + Section="CAT II (Medium)", + Severity="medium", + RuleID="SV-273186r1098825_rule", + StigID="OKTA-APP-000020", + CCI=["CCI-000057", "CCI-001133"], + CheckText="Verify the Global Session Policy logs out a session after 15 minutes of inactivity.", + FixText="From the Admin Console configure the Global Session Policy idle timeout to 15 minutes.", + ) + ], + Checks=["signon_global_session_idle_timeout_15min"], + ), + Compliance_Requirement( + Id="OKTA-APP-000650", + Name="Okta must enforce a minimum 15-character password length.", + Description="The shorter the password, the lower the number of possible combinations that need to be tested before the password is compromised.", + Attributes=[ + STIG_Requirement_Attribute( + Section="CAT II (Medium)", + Severity="medium", + RuleID="SV-273209r1098894_rule", + StigID="OKTA-APP-000650", + CCI=["CCI-000205"], + CheckText="Verify the password policy enforces a minimum length of 15 characters.", + FixText="From the Admin Console set the minimum password length to 15 characters.", + ) + ], + Checks=[], + ), + ], +) diff --git a/tests/lib/outputs/compliance/okta_idaas_stig/okta_idaas_stig_okta_test.py b/tests/lib/outputs/compliance/okta_idaas_stig/okta_idaas_stig_okta_test.py new file mode 100644 index 0000000000..fa616c906e --- /dev/null +++ b/tests/lib/outputs/compliance/okta_idaas_stig/okta_idaas_stig_okta_test.py @@ -0,0 +1,139 @@ +from datetime import datetime +from io import StringIO +from unittest import mock + +from freezegun import freeze_time +from mock import patch + +from prowler.lib.outputs.compliance.okta_idaas_stig.models import OktaIDaaSSTIGModel +from prowler.lib.outputs.compliance.okta_idaas_stig.okta_idaas_stig_okta import ( + OktaIDaaSSTIG, +) +from tests.lib.outputs.compliance.fixtures import OKTA_IDAAS_STIG_OKTA +from tests.lib.outputs.fixtures.fixtures import generate_finding_output + +OKTA_ORG_DOMAIN = "dev-12345.okta.com" + + +class TestOktaIDaaSSTIG: + def test_output_transform(self): + findings = [ + generate_finding_output( + provider="okta", + account_uid=OKTA_ORG_DOMAIN, + account_name=OKTA_ORG_DOMAIN, + region="global", + service_name="signon", + check_id="signon_global_session_idle_timeout_15min", + resource_uid="okta-global-session-policy", + resource_name="Default Policy", + compliance={"Okta-IDaaS-STIG-1R2": ["OKTA-APP-000020"]}, + ) + ] + + output = OktaIDaaSSTIG(findings, OKTA_IDAAS_STIG_OKTA) + output_data = output.data[0] + assert isinstance(output_data, OktaIDaaSSTIGModel) + assert output_data.Provider == "okta" + assert output_data.Framework == OKTA_IDAAS_STIG_OKTA.Framework + assert output_data.Name == OKTA_IDAAS_STIG_OKTA.Name + assert output_data.OrganizationDomain == OKTA_ORG_DOMAIN + assert output_data.Description == OKTA_IDAAS_STIG_OKTA.Description + assert output_data.Requirements_Id == OKTA_IDAAS_STIG_OKTA.Requirements[0].Id + assert ( + output_data.Requirements_Name == OKTA_IDAAS_STIG_OKTA.Requirements[0].Name + ) + assert ( + output_data.Requirements_Description + == OKTA_IDAAS_STIG_OKTA.Requirements[0].Description + ) + assert ( + output_data.Requirements_Attributes_Section + == OKTA_IDAAS_STIG_OKTA.Requirements[0].Attributes[0].Section + ) + assert ( + output_data.Requirements_Attributes_Severity + == OKTA_IDAAS_STIG_OKTA.Requirements[0].Attributes[0].Severity.value + ) + assert ( + output_data.Requirements_Attributes_RuleID + == OKTA_IDAAS_STIG_OKTA.Requirements[0].Attributes[0].RuleID + ) + assert ( + output_data.Requirements_Attributes_StigID + == OKTA_IDAAS_STIG_OKTA.Requirements[0].Attributes[0].StigID + ) + assert ( + output_data.Requirements_Attributes_CCI + == OKTA_IDAAS_STIG_OKTA.Requirements[0].Attributes[0].CCI + ) + assert ( + output_data.Requirements_Attributes_CheckText + == OKTA_IDAAS_STIG_OKTA.Requirements[0].Attributes[0].CheckText + ) + assert ( + output_data.Requirements_Attributes_FixText + == OKTA_IDAAS_STIG_OKTA.Requirements[0].Attributes[0].FixText + ) + assert output_data.Status == "PASS" + assert output_data.StatusExtended == "" + assert output_data.ResourceId == "okta-global-session-policy" + assert output_data.ResourceName == "Default Policy" + assert output_data.CheckId == "signon_global_session_idle_timeout_15min" + assert output_data.Muted is False + # Test manual check + output_data_manual = output.data[1] + assert output_data_manual.Provider == "okta" + assert output_data_manual.Framework == OKTA_IDAAS_STIG_OKTA.Framework + assert output_data_manual.Name == OKTA_IDAAS_STIG_OKTA.Name + assert output_data_manual.OrganizationDomain == "" + assert ( + output_data_manual.Requirements_Id + == OKTA_IDAAS_STIG_OKTA.Requirements[1].Id + ) + assert ( + output_data_manual.Requirements_Attributes_Severity + == OKTA_IDAAS_STIG_OKTA.Requirements[1].Attributes[0].Severity.value + ) + assert ( + output_data_manual.Requirements_Attributes_StigID + == OKTA_IDAAS_STIG_OKTA.Requirements[1].Attributes[0].StigID + ) + assert output_data_manual.Status == "MANUAL" + assert output_data_manual.StatusExtended == "Manual check" + assert output_data_manual.ResourceId == "manual_check" + assert output_data_manual.ResourceName == "Manual check" + assert output_data_manual.CheckId == "manual" + assert output_data_manual.Muted is False + + @freeze_time("2025-01-01 00:00:00") + @mock.patch( + "prowler.lib.outputs.compliance.okta_idaas_stig.okta_idaas_stig_okta.timestamp", + "2025-01-01 00:00:00", + ) + def test_batch_write_data_to_file(self): + mock_file = StringIO() + findings = [ + generate_finding_output( + provider="okta", + account_uid=OKTA_ORG_DOMAIN, + account_name=OKTA_ORG_DOMAIN, + region="global", + service_name="signon", + check_id="signon_global_session_idle_timeout_15min", + resource_uid="okta-global-session-policy", + resource_name="Default Policy", + compliance={"Okta-IDaaS-STIG-1R2": ["OKTA-APP-000020"]}, + ) + ] + output = OktaIDaaSSTIG(findings, OKTA_IDAAS_STIG_OKTA) + output._file_descriptor = mock_file + + with patch.object(mock_file, "close", return_value=None): + output.batch_write_data_to_file() + + mock_file.seek(0) + content = mock_file.read() + expected_csv = f"PROVIDER;DESCRIPTION;ORGANIZATIONDOMAIN;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_NAME;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_SECTION;REQUIREMENTS_ATTRIBUTES_SEVERITY;REQUIREMENTS_ATTRIBUTES_RULEID;REQUIREMENTS_ATTRIBUTES_STIGID;REQUIREMENTS_ATTRIBUTES_CCI;REQUIREMENTS_ATTRIBUTES_CHECKTEXT;REQUIREMENTS_ATTRIBUTES_FIXTEXT;STATUS;STATUSEXTENDED;RESOURCEID;RESOURCENAME;CHECKID;MUTED;FRAMEWORK;NAME\r\nokta;Defense Information Systems Agency (DISA) Security Technical Implementation Guide (STIG) for Okta Identity as a Service (IDaaS).;{OKTA_ORG_DOMAIN};{datetime.now()};OKTA-APP-000020;Okta must log out a session after a 15-minute period of inactivity.;A session timeout lock is a temporary action taken when a user stops work and moves away from the immediate vicinity of the information system.;CAT II (Medium);medium;SV-273186r1098825_rule;OKTA-APP-000020;['CCI-000057', 'CCI-001133'];Verify the Global Session Policy logs out a session after 15 minutes of inactivity.;From the Admin Console configure the Global Session Policy idle timeout to 15 minutes.;PASS;;okta-global-session-policy;Default Policy;signon_global_session_idle_timeout_15min;False;Okta-IDaaS-STIG;DISA Okta Identity as a Service (IDaaS) STIG V1R2\r\nokta;Defense Information Systems Agency (DISA) Security Technical Implementation Guide (STIG) for Okta Identity as a Service (IDaaS).;;{datetime.now()};OKTA-APP-000650;Okta must enforce a minimum 15-character password length.;The shorter the password, the lower the number of possible combinations that need to be tested before the password is compromised.;CAT II (Medium);medium;SV-273209r1098894_rule;OKTA-APP-000650;['CCI-000205'];Verify the password policy enforces a minimum length of 15 characters.;From the Admin Console set the minimum password length to 15 characters.;MANUAL;Manual check;manual_check;Manual check;manual;False;Okta-IDaaS-STIG;DISA Okta Identity as a Service (IDaaS) STIG V1R2\r\n" + + assert content == expected_csv diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index a3d12493d6..dc8c0fa708 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🚀 Added +- DISA Okta IDaaS STIG V1R2 compliance framework support with its dedicated mapper, details panel, and icon [(#11428)](https://github.com/prowler-cloud/prowler/pull/11428) - DORA compliance framework support [(#11131)](https://github.com/prowler-cloud/prowler/pull/11131) ### 🔄 Changed diff --git a/ui/components/compliance/compliance-custom-details/asd-essential-eight-details.tsx b/ui/components/compliance/compliance-custom-details/asd-essential-eight-details.tsx index 363eba2b10..2cb022a174 100644 --- a/ui/components/compliance/compliance-custom-details/asd-essential-eight-details.tsx +++ b/ui/components/compliance/compliance-custom-details/asd-essential-eight-details.tsx @@ -85,7 +85,7 @@ export const ASDEssentialEightCustomDetails = ({ )} @@ -93,7 +93,7 @@ export const ASDEssentialEightCustomDetails = ({ )} @@ -101,7 +101,7 @@ export const ASDEssentialEightCustomDetails = ({ )} diff --git a/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx b/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx index 96ed16a8f1..00aa51e4c5 100644 --- a/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx +++ b/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx @@ -52,7 +52,7 @@ export const AWSWellArchitectedCustomDetails = ({ )} @@ -60,7 +60,7 @@ export const AWSWellArchitectedCustomDetails = ({ )} @@ -68,7 +68,7 @@ export const AWSWellArchitectedCustomDetails = ({ )} diff --git a/ui/components/compliance/compliance-custom-details/ccc-details.tsx b/ui/components/compliance/compliance-custom-details/ccc-details.tsx index 309358264d..e3db174280 100644 --- a/ui/components/compliance/compliance-custom-details/ccc-details.tsx +++ b/ui/components/compliance/compliance-custom-details/ccc-details.tsx @@ -1,4 +1,4 @@ -import { cn } from "@/lib"; +import { Badge } from "@/components/shadcn/badge/badge"; import { CCC_MAPPING_SECTIONS, CCC_TEXT_SECTIONS } from "@/lib/compliance/ccc"; import { Requirement } from "@/types/compliance"; @@ -46,7 +46,7 @@ export const CCCCustomDetails = ({ requirement }: CCCDetailsProps) => { )} @@ -68,15 +68,9 @@ export const CCCCustomDetails = ({ requirement }: CCCDetailsProps) => {
{mapping.Identifiers.map((identifier, idx) => ( - + {identifier} - + ))}
diff --git a/ui/components/compliance/compliance-custom-details/cis-details.tsx b/ui/components/compliance/compliance-custom-details/cis-details.tsx index c46f061188..25823038d5 100644 --- a/ui/components/compliance/compliance-custom-details/cis-details.tsx +++ b/ui/components/compliance/compliance-custom-details/cis-details.tsx @@ -41,7 +41,7 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => { )} @@ -49,7 +49,7 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => { )} diff --git a/ui/components/compliance/compliance-custom-details/csa-details.tsx b/ui/components/compliance/compliance-custom-details/csa-details.tsx index 738e4a8aab..81dbcc158a 100644 --- a/ui/components/compliance/compliance-custom-details/csa-details.tsx +++ b/ui/components/compliance/compliance-custom-details/csa-details.tsx @@ -1,4 +1,4 @@ -import { cn } from "@/lib"; +import { Badge } from "@/components/shadcn/badge/badge"; import { CSA_MAPPING_SECTIONS } from "@/lib/compliance/csa"; import { Requirement } from "@/types/compliance"; @@ -36,28 +36,28 @@ export const CSACustomDetails = ({ requirement }: CSADetailsProps) => { )} {requirement.iaas && ( )} {requirement.paas && ( )} {requirement.saas && ( )} @@ -72,15 +72,9 @@ export const CSACustomDetails = ({ requirement }: CSADetailsProps) => {
{mapping.Identifiers.map((identifier, idx) => ( - + {identifier} - + ))}
diff --git a/ui/components/compliance/compliance-custom-details/dora-details.tsx b/ui/components/compliance/compliance-custom-details/dora-details.tsx index 3d84e891ba..f27412be5b 100644 --- a/ui/components/compliance/compliance-custom-details/dora-details.tsx +++ b/ui/components/compliance/compliance-custom-details/dora-details.tsx @@ -26,21 +26,21 @@ export const DORACustomDetails = ({ requirement }: DORADetailsProps) => { )} {requirement.article && ( )} {requirement.article_title && ( )} diff --git a/ui/components/compliance/compliance-custom-details/ens-details.tsx b/ui/components/compliance/compliance-custom-details/ens-details.tsx index 9e947cea14..2950c495cd 100644 --- a/ui/components/compliance/compliance-custom-details/ens-details.tsx +++ b/ui/components/compliance/compliance-custom-details/ens-details.tsx @@ -28,7 +28,7 @@ export const ENSCustomDetails = ({ )} @@ -36,7 +36,7 @@ export const ENSCustomDetails = ({ )} diff --git a/ui/components/compliance/compliance-custom-details/generic-details.tsx b/ui/components/compliance/compliance-custom-details/generic-details.tsx index caa3fec05d..323e3d9a99 100644 --- a/ui/components/compliance/compliance-custom-details/generic-details.tsx +++ b/ui/components/compliance/compliance-custom-details/generic-details.tsx @@ -26,7 +26,7 @@ export const GenericCustomDetails = ({ )} @@ -34,7 +34,7 @@ export const GenericCustomDetails = ({ )} @@ -42,7 +42,7 @@ export const GenericCustomDetails = ({ )} diff --git a/ui/components/compliance/compliance-custom-details/mitre-details.tsx b/ui/components/compliance/compliance-custom-details/mitre-details.tsx index 8fa7fa276d..53fc583f2c 100644 --- a/ui/components/compliance/compliance-custom-details/mitre-details.tsx +++ b/ui/components/compliance/compliance-custom-details/mitre-details.tsx @@ -37,7 +37,7 @@ export const MITRECustomDetails = ({ )} @@ -81,17 +81,17 @@ export const MITRECustomDetails = ({ {service.comment && ( diff --git a/ui/components/compliance/compliance-custom-details/okta-idaas-stig-details.tsx b/ui/components/compliance/compliance-custom-details/okta-idaas-stig-details.tsx new file mode 100644 index 0000000000..0869b7f390 --- /dev/null +++ b/ui/components/compliance/compliance-custom-details/okta-idaas-stig-details.tsx @@ -0,0 +1,65 @@ +import { Severity, SeverityBadge } from "@/components/ui/table"; +import { Requirement } from "@/types/compliance"; + +import { + ComplianceBadge, + ComplianceBadgeContainer, + ComplianceChipContainer, + ComplianceDetailContainer, + ComplianceDetailSection, + ComplianceDetailText, +} from "./shared-components"; + +export const OktaIDaaSStigCustomDetails = ({ + requirement, +}: { + requirement: Requirement; +}) => { + const severity = requirement.severity as string | undefined; + const stigId = requirement.stig_id as string | undefined; + const ruleId = requirement.rule_id as string | undefined; + const cci = requirement.cci as string[] | undefined; + const checkText = requirement.check_text as string | undefined; + const fixText = requirement.fix_text as string | undefined; + + return ( + + + {severity && ( +
+ + Severity: + + +
+ )} + {stigId && ( + + )} + {ruleId && ( + + )} +
+ + {requirement.description && ( + + {requirement.description} + + )} + + + + {checkText && ( + + {checkText} + + )} + + {fixText && ( + + {fixText} + + )} +
+ ); +}; diff --git a/ui/components/compliance/compliance-custom-details/shared-components.tsx b/ui/components/compliance/compliance-custom-details/shared-components.tsx index d5c69499af..22ba330d57 100644 --- a/ui/components/compliance/compliance-custom-details/shared-components.tsx +++ b/ui/components/compliance/compliance-custom-details/shared-components.tsx @@ -1,4 +1,12 @@ -import { cn } from "@/lib/utils"; +import { VariantProps } from "class-variance-authority"; + +import { Badge, badgeVariants } from "@/components/shadcn/badge/badge"; + +// Variants come straight from the canonical shadcn Badge so compliance panels +// share the same badge vocabulary (and tokens) as the rest of the app. +export type ComplianceBadgeVariant = NonNullable< + VariantProps["variant"] +>; export const ComplianceDetailContainer = ({ children, @@ -43,55 +51,28 @@ export const ComplianceBadgeContainer = ({ return
{children}
; }; -type BadgeColor = - | "red" // Risk/Level/Severity - | "blue" // Assessment/Method - | "orange" // Type/Category - | "green" // Weight/Score (positive) - | "purple" // Profile - | "indigo" // IDs/References - | "gray"; // Additional Info/Neutral - export const ComplianceBadge = ({ label, value, - color, + variant, conditional = false, }: { label: string; value: string | number; - color: BadgeColor; + variant: ComplianceBadgeVariant; conditional?: boolean; }) => { - const actualColor = conditional && Number(value) === 0 ? "gray" : color; - - const colorClasses = { - red: "bg-red-50 text-red-700 ring-red-600/10 dark:bg-red-400/10 dark:text-red-400 dark:ring-red-400/20", - blue: "bg-blue-50 text-blue-700 ring-blue-600/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/20", - orange: - "bg-orange-50 text-orange-700 ring-orange-600/10 dark:bg-orange-400/10 dark:text-orange-400 dark:ring-orange-400/20", - green: - "bg-green-50 text-green-700 ring-green-600/10 dark:bg-green-400/10 dark:text-green-400 dark:ring-green-400/20", - purple: - "bg-purple-50 text-purple-700 ring-purple-600/10 dark:bg-purple-400/10 dark:text-purple-400 dark:ring-purple-400/20", - indigo: - "bg-indigo-50 text-indigo-700 ring-indigo-600/10 dark:bg-indigo-400/10 dark:text-indigo-400 dark:ring-indigo-400/20", - gray: "bg-gray-50 text-gray-600 ring-gray-500/10 dark:bg-gray-400/10 dark:text-gray-400 dark:ring-gray-400/20", - }; + // A "conditional" metric badge with a zero value drops to a neutral variant + // so empty scores don't read as a meaningful (e.g. positive) result. + const actualVariant: ComplianceBadgeVariant = + conditional && Number(value) === 0 ? "secondary" : variant; return (
{label}: - - {value} - + {value}
); }; @@ -132,12 +113,9 @@ export const ComplianceChipContainer = ({
{items.map((item: string, index: number) => ( - + {item} - + ))}
diff --git a/ui/components/compliance/compliance-custom-details/threat-details.tsx b/ui/components/compliance/compliance-custom-details/threat-details.tsx index 822612234d..542583ff0e 100644 --- a/ui/components/compliance/compliance-custom-details/threat-details.tsx +++ b/ui/components/compliance/compliance-custom-details/threat-details.tsx @@ -34,7 +34,7 @@ export const ThreatCustomDetails = ({ )} @@ -42,7 +42,7 @@ export const ThreatCustomDetails = ({ )} @@ -50,7 +50,7 @@ export const ThreatCustomDetails = ({ )} @@ -61,13 +61,13 @@ export const ThreatCustomDetails = ({ {requirement.totalFindings > 0 && ( )} diff --git a/ui/components/icons/compliance/IconCompliance.test.ts b/ui/components/icons/compliance/IconCompliance.test.ts index ca5f5a363e..b956b80548 100644 --- a/ui/components/icons/compliance/IconCompliance.test.ts +++ b/ui/components/icons/compliance/IconCompliance.test.ts @@ -58,6 +58,12 @@ describe("getComplianceIcon", () => { expect(getComplianceIcon("prowler_threatscore_aws")).toBe(threatLogo); }); + it("resolves the Okta IDaaS STIG via the `okta` keyword", () => { + const oktaLogo = getComplianceIcon("Okta-IDaaS-STIG"); + expect(oktaLogo).toBeDefined(); + expect(getComplianceIcon("okta_idaas_stig_v1r2_okta")).toBe(oktaLogo); + }); + it("resolves ASD Essential Eight by the framework keyword, not by `aws`", () => { const essentialLogo = getComplianceIcon("ASD-Essential-Eight"); expect(essentialLogo).toBeDefined(); diff --git a/ui/components/icons/compliance/IconCompliance.tsx b/ui/components/icons/compliance/IconCompliance.tsx index fe493e2096..ab0fb9a36a 100644 --- a/ui/components/icons/compliance/IconCompliance.tsx +++ b/ui/components/icons/compliance/IconCompliance.tsx @@ -18,6 +18,7 @@ import KISALogo from "./kisa.svg"; import MITRELogo from "./mitre-attack.svg"; import NIS2Logo from "./nis2.svg"; import NISTLogo from "./nist.svg"; +import OktaLogo from "./okta.svg"; import PCILogo from "./pci-dss.svg"; import PROWLERTHREATLogo from "./prowlerThreat.svg"; import RBILogo from "./rbi.svg"; @@ -72,6 +73,7 @@ const COMPLIANCE_LOGOS = [ // compliance_id is just `dora`, no provider suffix. ["dora", DORALogo], ["secnumcloud", ANSSILogo], + ["okta", OktaLogo], ["aws", AWSLogo], ] as const; diff --git a/ui/components/icons/compliance/okta.svg b/ui/components/icons/compliance/okta.svg new file mode 100644 index 0000000000..91b8acbb20 --- /dev/null +++ b/ui/components/icons/compliance/okta.svg @@ -0,0 +1 @@ + diff --git a/ui/components/shadcn/badge/badge.test.tsx b/ui/components/shadcn/badge/badge.test.tsx new file mode 100644 index 0000000000..adae2f54b8 --- /dev/null +++ b/ui/components/shadcn/badge/badge.test.tsx @@ -0,0 +1,30 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { Badge } from "./badge"; + +describe("Badge", () => { + it("renders its children", () => { + render(Assessment); + expect(screen.getByText("Assessment")).toBeInTheDocument(); + }); + + it("applies the info variant token classes", () => { + const { container } = render(Info); + const badge = container.querySelector("[data-slot='badge']"); + // The info variant is built from the existing design-system blue token + // (bg-data-info) rather than a bespoke palette. + expect(badge?.className).toContain("bg-bg-data-info/15"); + expect(badge?.className).toContain("text-bg-data-info"); + }); + + it("merges a custom className", () => { + const { container } = render( + + Tag + , + ); + const badge = container.querySelector("[data-slot='badge']"); + expect(badge?.className).toContain("extra-class"); + }); +}); diff --git a/ui/components/shadcn/badge/badge.tsx b/ui/components/shadcn/badge/badge.tsx index f200fda7e2..fd1a6efd11 100644 --- a/ui/components/shadcn/badge/badge.tsx +++ b/ui/components/shadcn/badge/badge.tsx @@ -24,6 +24,7 @@ const badgeVariants = cva( "border-bg-warning/30 bg-bg-warning-secondary/20 text-text-warning-primary", error: "border-transparent bg-bg-fail-secondary text-text-error-primary", + info: "border-transparent bg-bg-data-info/15 text-bg-data-info", }, }, defaultVariants: { diff --git a/ui/lib/compliance/ccc.tsx b/ui/lib/compliance/ccc.tsx index fc6f7e898a..59df6a7c09 100644 --- a/ui/lib/compliance/ccc.tsx +++ b/ui/lib/compliance/ccc.tsx @@ -1,6 +1,7 @@ import { ClientAccordionContent } from "@/components/compliance/compliance-accordion/client-accordion-content"; import { ComplianceAccordionRequirementTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title"; import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title"; +import { ComplianceBadgeVariant } from "@/components/compliance/compliance-custom-details/shared-components"; import { AccordionItemProps } from "@/components/ui/accordion/Accordion"; import { FindingStatus } from "@/components/ui/table/status-finding-badge"; import { @@ -39,7 +40,7 @@ export interface CCCTextSection { export interface CCCMappingSection { title: string; key: keyof Requirement; - colorClasses: string; + variant: ComplianceBadgeVariant; } export const CCC_TEXT_SECTIONS: CCCTextSection[] = [ @@ -71,14 +72,12 @@ export const CCC_MAPPING_SECTIONS: CCCMappingSection[] = [ { title: "Threat Mappings", key: "section_threat_mappings", - colorClasses: - "bg-red-50 text-red-700 ring-red-600/10 dark:bg-red-400/10 dark:text-red-400 dark:ring-red-400/20", + variant: "error", }, { title: "Guideline Mappings", key: "section_guideline_mappings", - colorClasses: - "bg-blue-50 text-blue-700 ring-blue-600/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/20", + variant: "info", }, ]; diff --git a/ui/lib/compliance/compliance-mapper.test.ts b/ui/lib/compliance/compliance-mapper.test.ts index 9526b50b07..25933b8dff 100644 --- a/ui/lib/compliance/compliance-mapper.test.ts +++ b/ui/lib/compliance/compliance-mapper.test.ts @@ -62,6 +62,10 @@ vi.mock( "@/components/compliance/compliance-custom-details/mitre-details", () => ({ MITRECustomDetails: stubFactory("MITREStub") }), ); +vi.mock( + "@/components/compliance/compliance-custom-details/okta-idaas-stig-details", + () => ({ OktaIDaaSStigCustomDetails: stubFactory("OktaIDaaSStigStub") }), +); vi.mock( "@/components/compliance/compliance-custom-details/threat-details", () => ({ ThreatCustomDetails: stubFactory("ThreatStub") }), @@ -144,6 +148,7 @@ describe("getComplianceMapper", () => { { framework: "ProwlerThreatScore", expected: "ThreatStub" }, { framework: "CCC", expected: "CCCStub" }, { framework: "CSA-CCM", expected: "CSAStub" }, + { framework: "Okta-IDaaS-STIG", expected: "OktaIDaaSStigStub" }, ]; for (const { framework, expected } of wiring) { @@ -188,6 +193,7 @@ describe("getComplianceMapper", () => { "ProwlerThreatScore", "CCC", "CSA-CCM", + "Okta-IDaaS-STIG", ]) { const mapper = getComplianceMapper(framework); expect(Object.keys(mapper).sort(), framework).toEqual(expectedKeys); diff --git a/ui/lib/compliance/compliance-mapper.ts b/ui/lib/compliance/compliance-mapper.ts index ba8c97ecfc..2e44958c73 100644 --- a/ui/lib/compliance/compliance-mapper.ts +++ b/ui/lib/compliance/compliance-mapper.ts @@ -12,6 +12,7 @@ import { GenericCustomDetails } from "@/components/compliance/compliance-custom- import { ISOCustomDetails } from "@/components/compliance/compliance-custom-details/iso-details"; import { KISACustomDetails } from "@/components/compliance/compliance-custom-details/kisa-details"; import { MITRECustomDetails } from "@/components/compliance/compliance-custom-details/mitre-details"; +import { OktaIDaaSStigCustomDetails } from "@/components/compliance/compliance-custom-details/okta-idaas-stig-details"; import { ThreatCustomDetails } from "@/components/compliance/compliance-custom-details/threat-details"; import { AccordionItemProps } from "@/components/ui/accordion/Accordion"; import { @@ -74,6 +75,10 @@ import { mapComplianceData as mapMITREComplianceData, toAccordionItems as toMITREAccordionItems, } from "./mitre"; +import { + mapComplianceData as mapOktaIDaaSStigComplianceData, + toAccordionItems as toOktaIDaaSStigAccordionItems, +} from "./okta-idaas-stig"; import { getTopFailedSections as getThreatScoreTopFailedSections, mapComplianceData as mapThetaComplianceData, @@ -213,6 +218,15 @@ const getComplianceMappers = (): Record => ({ getDetailsComponent: (requirement: Requirement) => createElement(CSACustomDetails, { requirement }), }, + "Okta-IDaaS-STIG": { + mapComplianceData: mapOktaIDaaSStigComplianceData, + toAccordionItems: toOktaIDaaSStigAccordionItems, + getTopFailedSections, + calculateCategoryHeatmapData: (data: Framework[]) => + calculateCategoryHeatmapData(data), + getDetailsComponent: (requirement: Requirement) => + createElement(OktaIDaaSStigCustomDetails, { requirement }), + }, // DORA (Regulation (EU) 2022/2554) — universal framework keyed by the // `framework` field of `prowler/compliance/dora.json` ("DORA"). Groups by // Pillar (5 enum values) and surfaces Pillar / Article / ArticleTitle in diff --git a/ui/lib/compliance/csa.tsx b/ui/lib/compliance/csa.tsx index 93974d6b47..c4624baa43 100644 --- a/ui/lib/compliance/csa.tsx +++ b/ui/lib/compliance/csa.tsx @@ -1,6 +1,7 @@ import { ClientAccordionContent } from "@/components/compliance/compliance-accordion/client-accordion-content"; import { ComplianceAccordionRequirementTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title"; import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title"; +import { ComplianceBadgeVariant } from "@/components/compliance/compliance-custom-details/shared-components"; import { AccordionItemProps } from "@/components/ui/accordion/Accordion"; import { FindingStatus } from "@/components/ui/table/status-finding-badge"; import { @@ -24,15 +25,14 @@ import { export interface CSAMappingSection { title: string; key: keyof Requirement; - colorClasses: string; + variant: ComplianceBadgeVariant; } export const CSA_MAPPING_SECTIONS: CSAMappingSection[] = [ { title: "Scope Applicability", key: "scope_applicability", - colorClasses: - "bg-blue-50 text-blue-700 ring-blue-600/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/20", + variant: "info", }, ]; diff --git a/ui/lib/compliance/okta-idaas-stig.tsx b/ui/lib/compliance/okta-idaas-stig.tsx new file mode 100644 index 0000000000..5a00886406 --- /dev/null +++ b/ui/lib/compliance/okta-idaas-stig.tsx @@ -0,0 +1,163 @@ +import { ClientAccordionContent } from "@/components/compliance/compliance-accordion/client-accordion-content"; +import { ComplianceAccordionRequirementTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title"; +import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title"; +import { AccordionItemProps } from "@/components/ui/accordion/Accordion"; +import { FindingStatus } from "@/components/ui/table/status-finding-badge"; +import { + AttributesData, + Control, + Framework, + isOktaIDaaSStigAttributesMetadata, + OktaIDaaSStigRequirement, + Requirement, + REQUIREMENT_STATUS, + RequirementsData, + RequirementStatus, +} from "@/types/compliance"; + +import { + calculateFrameworkCounters, + createRequirementsMap, + findOrCreateCategory, + findOrCreateControl, + findOrCreateFramework, +} from "./commons"; + +const getStatusCounters = (status: RequirementStatus) => ({ + pass: status === REQUIREMENT_STATUS.PASS ? 1 : 0, + fail: status === REQUIREMENT_STATUS.FAIL ? 1 : 0, + manual: status === REQUIREMENT_STATUS.MANUAL ? 1 : 0, +}); + +export const mapComplianceData = ( + attributesData: AttributesData, + requirementsData: RequirementsData, +): Framework[] => { + const attributes = attributesData?.data || []; + const requirementsMap = createRequirementsMap(requirementsData); + const frameworks: Framework[] = []; + + for (const attributeItem of attributes) { + const id = attributeItem.id; + const metadataArray = attributeItem.attributes?.attributes?.metadata; + const attrs = metadataArray?.[0]; + if (!isOktaIDaaSStigAttributesMetadata(attrs)) continue; + + const requirementData = requirementsMap.get(id); + if (!requirementData) continue; + + const frameworkName = attributeItem.attributes.framework; + // Level 1: Section maps to the STIG severity category (e.g. "CAT II (Medium)") + const categoryName = attrs.Section; + // Level 2: each requirement is its own control, labelled by its STIG ID + const controlLabel = id; + const description = attributeItem.attributes.description; + // Human-readable STIG title (e.g. "Okta must log out a session after a + // 15-minute period of inactivity."). Surface it next to the STIG ID and + // fall back to the bare ID when missing, mirroring DORA/CSA. + const requirementName = attributeItem.attributes.name || ""; + const status = requirementData.attributes.status || ""; + const checks = attributeItem.attributes.attributes.check_ids || []; + + const framework = findOrCreateFramework(frameworks, frameworkName); + const category = findOrCreateCategory(framework.categories, categoryName); + const control = findOrCreateControl(category.controls, controlLabel); + + const finalStatus: RequirementStatus = status as RequirementStatus; + const requirement = { + name: requirementName ? `${id} - ${requirementName}` : id, + description, + status: finalStatus, + check_ids: checks, + ...getStatusCounters(finalStatus), + severity: attrs.Severity, + rule_id: attrs.RuleID, + stig_id: attrs.StigID, + cci: attrs.CCI, + check_text: attrs.CheckText, + fix_text: attrs.FixText, + } satisfies OktaIDaaSStigRequirement; + + control.requirements.push(requirement); + } + + calculateFrameworkCounters(frameworks); + + return frameworks; +}; + +const createRequirementItem = ( + requirement: Requirement, + frameworkName: string, + categoryName: string, + controlIndex: number, + scanId: string, +): AccordionItemProps => ({ + key: `${frameworkName}-${categoryName}-control-${controlIndex}`, + title: ( + + ), + content: ( + + ), + items: [], +}); + +const createControlItem = ( + control: Control, + frameworkName: string, + categoryName: string, + controlIndex: number, + scanId: string, +): AccordionItemProps => + createRequirementItem( + control.requirements[0], + frameworkName, + categoryName, + controlIndex, + scanId, + ); + +export const toAccordionItems = ( + data: Framework[], + scanId: string | undefined, +): AccordionItemProps[] => { + const safeId = scanId || ""; + + return data.flatMap((framework) => + framework.categories.map((category) => ({ + key: `${framework.name}-${category.name}`, + title: ( + + ), + content: "", + items: category.controls.map((control, controlIndex) => + createControlItem( + control, + framework.name, + category.name, + controlIndex, + safeId, + ), + ), + })), + ); +}; diff --git a/ui/types/compliance.ts b/ui/types/compliance.ts index e926efe356..fde9d260e9 100644 --- a/ui/types/compliance.ts +++ b/ui/types/compliance.ts @@ -279,6 +279,12 @@ const isOneOf = ( const isStringArray = (value: unknown): value is string[] => Array.isArray(value) && value.every((item) => typeof item === "string"); +const isOptionalString = (value: unknown): value is string | undefined => + value === undefined || typeof value === "string"; + +const isOptionalStringArray = (value: unknown): value is string[] | undefined => + value === undefined || isStringArray(value); + const ASD_METADATA_STRING_FIELDS = [ "Section", "Description", @@ -327,6 +333,43 @@ export interface ASDEssentialEightRequirement extends Requirement { references: ASDEssentialEightAttributesMetadata["References"]; } +export interface OktaIDaaSStigAttributesMetadata { + Section: string; + Severity: string; + RuleID: string; + StigID: string; + CCI?: string[]; + CheckText?: string; + FixText?: string; +} + +const OKTA_IDAAS_STIG_REQUIRED_STRING_FIELDS = [ + "Section", + "Severity", + "RuleID", + "StigID", +] as const satisfies readonly (keyof OktaIDaaSStigAttributesMetadata)[]; + +export const isOktaIDaaSStigAttributesMetadata = ( + value: unknown, +): value is OktaIDaaSStigAttributesMetadata => + isRecord(value) && + OKTA_IDAAS_STIG_REQUIRED_STRING_FIELDS.every( + (field) => typeof value[field] === "string", + ) && + isOptionalStringArray(value.CCI) && + isOptionalString(value.CheckText) && + isOptionalString(value.FixText); + +export interface OktaIDaaSStigRequirement extends Requirement { + severity: OktaIDaaSStigAttributesMetadata["Severity"]; + rule_id: OktaIDaaSStigAttributesMetadata["RuleID"]; + stig_id: OktaIDaaSStigAttributesMetadata["StigID"]; + cci: OktaIDaaSStigAttributesMetadata["CCI"]; + check_text: OktaIDaaSStigAttributesMetadata["CheckText"]; + fix_text: OktaIDaaSStigAttributesMetadata["FixText"]; +} + // DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554). // Universal framework — flat attributes dict with Pillar/Article/ArticleTitle. // `Pillar` is the canonical grouping key for tables and PDF; the enum mirrors @@ -374,6 +417,7 @@ export interface AttributesItemData { | CCCAttributesMetadata[] | CSAAttributesMetadata[] | ASDEssentialEightAttributesMetadata[] + | OktaIDaaSStigAttributesMetadata[] | DORAAttributesMetadata[] | GenericAttributesMetadata[]; check_ids: string[];