Skip to content

feat: add rule AZ-DB-008 SQL Server minimum TLS version below 1.2 - #361

Open
dipeshrayg wants to merge 3 commits into
OWASP:devfrom
dipeshrayg:feat/az-db-008-sql-minimum-tls
Open

dipeshrayg wants to merge 3 commits into
OWASP:devfrom
dipeshrayg:feat/az-db-008-sql-minimum-tls

Conversation

@dipeshrayg

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds rule AZ-DB-008: detects Azure SQL Servers that do not enforce a minimum TLS version of 1.2.

Type of change

  • New scan rule
  • Remediation playbook
  • Compliance mapping

Rule details

  • Rule ID: AZ-DB-008
  • Severity: HIGH
  • Category: Database
  • Frameworks mapped: NIST / ISO 27001 / SOC 2 (CIS has no numbered control for this specific setting, see below)

Testing

  • Returns correct JSON output
  • All seven CI checks pass locally
  • No hardcoded credentials or secrets
  • Tested against a real Azure free trial subscription (no live subscription available in this environment)

Notes on scoping

Checked the actual installed azure-mgmt-sql SDK model before writing this: Server.minimal_tls_version is a plain string (not an enum), and the real Azure API allows an explicit "None" value alongside "1.0"/"1.1"/"1.2"/"1.3", different from the TLS1_0-style values some other Azure services use. The rule and tests match that, flagging a missing attribute, an explicit "None", "1.0", or "1.1".

Verified against the official CIS Azure Foundations Benchmark 2.0.0 control mapping that no numbered control exists for this SQL-server-specific setting (only the storage-account minimum TLS control, 3.15, is numbered, and it doesn't apply to Microsoft.Sql/servers), so this follows the repo's existing N/A-* convention already used for AZ-STOR-006 through AZ-STOR-009.

Related issue

Closes #246

Checklist

  • Every commit includes a DCO Signed-off-by trailer
  • My code follows the rule template in CONTRIBUTING.md
  • I added the matching CLI playbook
  • I added all four compliance framework mappings
  • I have not committed any real Azure credentials
  • My branch name follows the convention: feat/description

@parthrohit22

Copy link
Copy Markdown
Collaborator

Hi @dipeshrayg, thanks for picking up #246! Before I do a full review, could you test this against a real Azure environment? Create a SQL server, set its minimum TLS to different values, run the scanner, then run the playbook. It'll help you understand both your change and how OpenShield behaves end to end. You can get free Azure credits with a student account (Azure for Students).
One thing worth checking there: since Microsoft retired TLS 1.0/1.1 for Azure SQL, what does minimalTlsVersion actually return for new and existing servers, and does the rule still fire the way the tests assume?
If that isn't possible for you, just let me know and I'll review it thoroughly as is. Thanks!

TFT444
TFT444 previously approved these changes Sep 27, 2026

@TFT444 TFT444 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed f2aa197. CI all green. Approving.

Rule (az_db_008.py)

  • _COMPLIANT_VERSIONS = {"1.2", "1.3"} is the right set: future TLS 1.3 adoption is pre-allowed without needing a code change.
  • getattr(server, "minimal_tls_version", None) correctly handles a missing attribute as non-compliant. The inline comment explaining that Azure's API allows the string "None" (distinct from Python None) is exactly the kind of why-not-what comment this codebase needs.
  • CIS N/A convention is correctly applied with a clear justification citing the actual CIS Azure Foundations Benchmark 2.0.0 document. The CIS description explains why no numbered control exists (storage-account TLS 3.15 does not apply to Microsoft.Sql/servers).
  • Framework mappings are accurate: NIST PR.DS-2 (data-in-transit protection), ISO 27001 A.10.1.1 (cryptographic controls policy), SOC 2 CC6.7 (protects data in transit). All four match the rule's intent without overreach.
  • enisa_pqc.json and ncsc_pqc.json are PQC-specific frameworks; correctly not modified here.

Playbook (fix_az_db_008.sh)

  • set -euo pipefail is set.
  • Empty-server guard prevents a no-op run from producing misleading output.
  • The TLS check allows 1.2 or 1.3 before remediating, consistent with _COMPLIANT_VERSIONS.
  • --minimal-tls-version 1.2 (not 1.3) is the right default: remediation brings the server to the minimum acceptable baseline, not the maximum.

Tests (test_rules_database.py)
Six cases cover: compliant 1.2, compliant 1.3, non-compliant 1.0, non-compliant 1.1, explicit string "None", and missing attribute. Every flagged case asserts rule_id, severity, category, resource_type, and the metadata.minimal_tls_version payload. The _REQUIRED_FIELDS assertion on the 1.0 case verifies the full finding schema.

@parthrohit22 parthrohit22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please handle failures from az sql server show separately from a successfully retrieved TLS value. The current || echo "" fallback treats read errors as non-compliant configuration and may trigger an update without verifying the server’s state. Consider reporting the error and skipping that server; remediate only after a successful read confirms the TLS version is below 1.2.

Detects Azure SQL Servers whose minimal_tls_version is missing, an
explicit "None", or below 1.2 ("1.0"/"1.1"), matching the real
azure-mgmt-sql Server model (a plain string, not the TLS1_0-style
enum some other Azure services use).

No numbered CIS Azure Foundations control exists for this specific
setting (verified against the official 2.0.0 control mapping; only
the storage-account minimum TLS control, 3.15, is numbered, and it
does not apply to Microsoft.Sql/servers), so this follows the repo's
existing N/A-* convention like AZ-STOR-006 through AZ-STOR-009.

Closes OWASP#246

Signed-off-by: Dipesh Ray <dipesh.ray.g@gmail.com>
The previous '|| echo ""' fallback made a failed 'az sql server show'
(permissions, transient API error) indistinguishable from a server with
no minimum TLS set, so the playbook could update a server without ever
confirming its state. A failed read now reports an error, skips that
server, and makes the script exit non-zero; only a successful read of
an unset/None/below-1.2 value is remediated.

Signed-off-by: Dipesh Ray <dipesh.ray.g@gmail.com>
TFT444
TFT444 previously approved these changes Oct 3, 2026

@TFT444 TFT444 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@parthrohit22's requested change has been addressed. The || echo "" fallback has been replaced with a proper if ! guard that catches read failures, logs an error, increments a READ_FAILURES counter, and skips the server via continue. No update is ever made without a confirmed successful read. The script also exits with code 1 if any reads failed, so CI or operators see the failure rather than a silent partial run.

Rule, tests, and compliance mappings were already sound in the previous review. Approving.

@TFT444

TFT444 commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

@parthrohit22 the playbook fix addresses your earlier request-changes. Could you re-review when you get a chance? Thanks.

ritiksah141
ritiksah141 previously approved these changes Oct 3, 2026

@ritiksah141 ritiksah141 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving it.

"""Detect Azure SQL Servers whose minimum TLS version is below 1.2 or unset."""
findings: List[Dict[str, Any]] = []

for server in azure_client.get_sql_servers():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AzureClient.get_sql_servers() catches SDK/API exceptions and returns [], so this loop cannot distinguish a successful empty inventory from an authorization, network, or throttling failure. In the latter case AZ-DB-008 reports no findings and can make an incomplete scan look clean. Please preserve an indeterminate/error state for inventory failures and avoid treating it as a compliant empty result.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR notes that testing against a live Azure subscription was not performed because no subscription was available in the contributor’s environment. I had previously asked [@dipeshrayg](https://github.com/dipeshrayg) to test the change in Azure or let us know if they were unable to do so. Could you please confirm whether you can perform this manual test? If not, please let us know so we can determine how to proceed.

…nventory is UNKNOWN

AzureClient.get_sql_servers() returns [] on any API failure as well as for
an empty subscription, so scan() alone made an unreadable inventory look
like a clean result. Add evaluate() per the OWASP#263 contract: PASS/FAIL per
server, and UNKNOWN (never a pass) when no servers are returned. scan() is
unchanged apart from sharing the finding builder.

Signed-off-by: Dipesh Ray <dipesh.ray.g@gmail.com>
@dipeshrayg
dipeshrayg dismissed stale reviews from ritiksah141 and TFT444 via e63825a October 4, 2026 10:41
@dipeshrayg

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Pushed three changes:

  • Playbook read errors: a failed az sql server show no longer falls through to an update. It reports an error, skips that server with no change made, and the script exits non-zero at the end. Only a successful read returning unset/None/below 1.2 is remediated. I checked it against a stubbed az for the three cases (read failure, below 1.2, already compliant) and the failing server got no update call.
  • Inventory failures (inline comment on az_db_008.py): agreed, get_sql_servers() returns [] for an API failure and for a genuinely empty subscription, so the rule could not tell them apart. I added evaluate() following the feat: persist PASS/FAIL/ERROR/NOT_APPLICABLE per rule per resource, fix compliance score #263 contract: PASS/FAIL per server, and UNKNOWN (INVENTORY_EMPTY_OR_UNAVAILABLE) when no servers come back, which never counts as a pass in compliance scoring. scan() still returns no findings in that case, since a findings list cannot express "indeterminate", so the unknown state lives in the evaluation record. Tests cover PASS/FAIL per server, an unset TLS value, and the failed/empty inventory case. Making get_sql_servers() itself distinguish failure from empty would touch the other SQL rules that call it, so I left that out of this PR.
  • Merge conflict: rebased onto current dev. The compliance files moved to the mapping-pack schema since this was opened, so the four AZ-DB-008 entries are rewritten to match (CIS N/A-DB-008 as not_applicable like AZ-STOR-007, the rest supporting, all pending_review). validate_mapping_pack.py passes.

Full suite, ruff and the mapping validator pass locally. I'll come back separately on the live Azure test question.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add rule for Azure SQL TLS version enforcement (AZ-SQL-TLS-001)

4 participants