Skip to content

Update AzureSecurityBenchmark package and workbooks#14039

Merged
v-atulyadav merged 3 commits intomasterfrom
v-sabiraj-azurebenchmark
Apr 13, 2026
Merged

Update AzureSecurityBenchmark package and workbooks#14039
v-atulyadav merged 3 commits intomasterfrom
v-sabiraj-azurebenchmark

Conversation

@v-sabiraj
Copy link
Copy Markdown
Contributor

Refresh AzureSecurityBenchmark solution artifacts: update packaged 3.0.4.zip and the workbook JSON. Add an empty "requiredDataConnectors": [] entry in mainTemplate.json and update the serialized workbook content in mainTemplate.json (and the Workbooks/AzureSecurityBenchmark.json) to sync the ARM template with the latest workbook changes.

Required items, please complete

Change(s):

  • See guidance below

Reason for Change(s):

  • See guidance below

Version Updated:

  • Required only for Detections/Analytic Rule templates
  • See guidance below

Testing Completed:

  • See guidance below

Checked that the validations are passing and have addressed any issues that are present:

  • See guidance below

Guidance <- remove section before submitting


Before submitting this PR please ensure that you have read the following sections and filled out the changes, reason for change and testing complete sections:

Thank you for your contribution to the Microsoft Sentinel Github repo.

Details of the code changes in your submitted PR. Providing descriptions for pull requests ensures there is context to changes being made and greatly enhances the code review process. Providing associated Issues that this resolves also easily connects the reason.

Change(s):

  • Updated syntax for XYZ.yaml

Reason for Change(s):

Version updated:

  • Yes
  • Detections/Analytic Rule templates are required to have the version updated

The code should have been tested in a Microsoft Sentinel environment that does not have any custom parsers, functions or tables, so that you validate no incorrect syntax and execution functions properly. If your submission requires a custom parser or function, it must be submitted with the PR.

Testing Completed:

  • Yes/No/Need Help

Note: If updating a detection, you must update the version field.

Before the submission has been made, please look at running the KQL and Yaml Validation Checks locally.
https://github.com/Azure/Azure-Sentinel#run-kql-validation-locally

Checked that the validations are passing and have addressed any issues that are present:

  • Yes/No/Need Help

Note: Let us know if you have tried fixing the validation error and need help.

References:


Refresh AzureSecurityBenchmark solution artifacts: update packaged 3.0.4.zip and the workbook JSON. Add an empty "requiredDataConnectors": [] entry in mainTemplate.json and update the serialized workbook content in mainTemplate.json (and the Workbooks/AzureSecurityBenchmark.json) to sync the ARM template with the latest workbook changes.
@v-sabiraj v-sabiraj requested review from a team as code owners April 10, 2026 13:31
@v-shukore v-shukore self-assigned this Apr 13, 2026
@v-shukore v-shukore added the Solution Solution specialty review needed label Apr 13, 2026
@v-sabiraj v-sabiraj requested a review from Copilot April 13, 2026 08:17
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Refreshes AzureSecurityBenchmark workbook content to align with the latest packaged/ARM template artifacts, including updates to KQL that rely on the selected ComplianceDomain parameter.

Changes:

  • Updated multiple workbook KQL queries to filter using the {ComplianceDomain} parameter list.
  • Synced workbook query logic changes across workbook visualizations.

"content": {
"version": "KqlItem/1.0",
"query": "securityresources\r\n | where type == \"microsoft.security/regulatorycompliancestandards/regulatorycompliancecontrols/regulatorycomplianceassessments\"\r\n | extend complianceStandardId = replace( \"-\", \" \", extract(@'/regulatoryComplianceStandards/([^/]*)', 1, id))\r\n | where complianceStandardId == \"Microsoft cloud security benchmark\"\r\n | join kind = leftouter(\r\n securityresources\r\n | where type == \"microsoft.security/assessments\") on subscriptionId, name\r\n | extend complianceState = properties.state\r\n | extend resourceSource = tolower(tostring(properties1.resourceDetails.Source))\r\n | extend recommendationId = id1\r\n | extend resourceId = trim(' ', tolower(tostring(case(resourceSource =~ 'azure', properties1.resourceDetails.Id,\r\n resourceSource =~ 'gcp', properties1.resourceDetails.GcpResourceId,\r\n resourceSource =~ 'aws', properties1.resourceDetails.AwsResourceId,\r\n extract('^(.+)/providers/Microsoft.Security/assessments/.+$',1,recommendationId)))))\r\n | extend regexResourceId = extract_all(@\"/providers/[^/]+(?:/([^/]+)/[^/]+(?:/[^/]+/[^/]+)?)?/([^/]+)/([^/]+)$\", resourceId)[0]\r\n | extend resourceType = iff(regexResourceId[1] != \"\", regexResourceId[1], iff(regexResourceId[0] != \"\", regexResourceId[0], \"subscriptions\"))\r\n | extend resourceName = regexResourceId[2]\r\n | extend recommendationName = name\r\n | extend RecommendationName = properties1.displayName\r\n | extend description = properties1.metadata.description\r\n | extend remediationSteps = properties1.metadata.remediationDescription\r\n | extend severity = properties1.metadata.severity\r\n | extend state = properties1.status.code\r\n | extend notApplicableReason = properties1.status.cause\r\n | extend RecommendationLink = properties1.links.azurePortal\r\n | extend complianceStandardId = replace( \"-\", \" \", extract(@'/regulatoryComplianceStandards/([^/]*)', 1, id))\r\n | extend complianceControlId = extract(@\"/regulatoryComplianceControls/([^/]*)\", 1, id)\r\n | join kind = leftouter (securityresources\r\n | where type == \"microsoft.security/regulatorycompliancestandards/regulatorycompliancecontrols\"\r\n | extend complianceStandardId = replace( \"-\", \" \", extract(@'/regulatoryComplianceStandards/([^/]*)', 1, id))\r\n | where complianceStandardId == \"Microsoft cloud security benchmark\"\r\n | extend controlName = tostring(properties.description)\r\n | project controlId = name, controlName\r\n | distinct *) on $right.controlId == $left.complianceControlId\r\n | extend RecommendationName = tostring(properties.description)\r\n | summarize Failed = countif(state == \"Unhealthy\"), Passed = countif(state == \"Healthy\" or complianceState == \"Passed\"), Total = countif(state == \"Unhealthy\" or state == \"Healthy\" or complianceState == \"Passed\") by RecommendationName, ControlID = controlId\r\n | extend PassedControls = (Passed/todouble(Total))*100\r\n| join kind = leftouter (securityresources\r\n | where type == \"microsoft.security/regulatorycompliancestandards/regulatorycompliancecontrols/regulatorycomplianceassessments\"\r\n | extend complianceStandardId = replace( \"-\", \" \", extract(@'/regulatoryComplianceStandards/([^/]*)', 1, id))\r\n | where complianceStandardId == \"Microsoft cloud security benchmark\"\r\n | extend RecommendationName = tostring(properties.description)\r\n | extend RecommendationLink = tostring(properties.assessmentDetailsLink)\r\n | project RecommendationName, RecommendationLink, name) on RecommendationName\r\n| extend ComplianceDomain=iff(ControlID contains \"AM.\", \"Asset Management\", iff(ControlID contains \"BR.\", \"Backup & Recovery\", iff(ControlID contains \"DP.\", \"Data Protection\", iff(ControlID contains \"DS.\", \"DevOps Security\", iff(ControlID contains \"ES.\", \"Endpoint Security\", iff(ControlID contains \"GS.\", \"Governance & Strategy\", iff(ControlID contains \"IM.\", \"Identity Management\", iff(ControlID contains \"IR.\", \"Incident Response\", iff(ControlID contains \"LT.\", \"Logging & Threat Detection\", iff(ControlID contains \"NS.\", \"Network Security\", iff(ControlID contains \"PA.\", \"Privileged Access\", iff(ControlID contains \"PV.\", \"Posture & Vulnerability Management\",\"Other\"))))))))))))\r\n| where ComplianceDomain has (ComplianceDomain) \r\n| extend Remediate=RecommendationLink\r\n| parse Remediate with * 'portal.azure.com/#blade/Microsoft_Azure_Security/RecommendationsBlade/assessmentKey/' assessmentKey '/' *\r\n| distinct RecommendationName, Total, Remediate, PassedControls, Passed, Failed, assessmentKey\r\n| sort by Total, Passed desc\r\n| limit 250",
"query": "securityresources\r\n | where type == \"microsoft.security/regulatorycompliancestandards/regulatorycompliancecontrols/regulatorycomplianceassessments\"\r\n | extend complianceStandardId = replace( \"-\", \" \", extract(@'/regulatoryComplianceStandards/([^/]*)', 1, id))\r\n | where complianceStandardId == \"Microsoft cloud security benchmark\"\r\n | join kind = leftouter(\r\n securityresources\r\n | where type == \"microsoft.security/assessments\") on subscriptionId, name\r\n | extend complianceState = properties.state\r\n | extend resourceSource = tolower(tostring(properties1.resourceDetails.Source))\r\n | extend recommendationId = id1\r\n | extend resourceId = trim(' ', tolower(tostring(case(resourceSource =~ 'azure', properties1.resourceDetails.Id,\r\n resourceSource =~ 'gcp', properties1.resourceDetails.GcpResourceId,\r\n resourceSource =~ 'aws', properties1.resourceDetails.AwsResourceId,\r\n extract('^(.+)/providers/Microsoft.Security/assessments/.+$',1,recommendationId)))))\r\n | extend regexResourceId = extract_all(@\"/providers/[^/]+(?:/([^/]+)/[^/]+(?:/[^/]+/[^/]+)?)?/([^/]+)/([^/]+)$\", resourceId)[0]\r\n | extend resourceType = iff(regexResourceId[1] != \"\", regexResourceId[1], iff(regexResourceId[0] != \"\", regexResourceId[0], \"subscriptions\"))\r\n | extend resourceName = regexResourceId[2]\r\n | extend recommendationName = name\r\n | extend RecommendationName = properties1.displayName\r\n | extend description = properties1.metadata.description\r\n | extend remediationSteps = properties1.metadata.remediationDescription\r\n | extend severity = properties1.metadata.severity\r\n | extend state = properties1.status.code\r\n | extend notApplicableReason = properties1.status.cause\r\n | extend RecommendationLink = properties1.links.azurePortal\r\n | extend complianceStandardId = replace( \"-\", \" \", extract(@'/regulatoryComplianceStandards/([^/]*)', 1, id))\r\n | extend complianceControlId = extract(@\"/regulatoryComplianceControls/([^/]*)\", 1, id)\r\n | join kind = leftouter (securityresources\r\n | where type == \"microsoft.security/regulatorycompliancestandards/regulatorycompliancecontrols\"\r\n | extend complianceStandardId = replace( \"-\", \" \", extract(@'/regulatoryComplianceStandards/([^/]*)', 1, id))\r\n | where complianceStandardId == \"Microsoft cloud security benchmark\"\r\n | extend controlName = tostring(properties.description)\r\n | project controlId = name, controlName\r\n | distinct *) on $right.controlId == $left.complianceControlId\r\n | extend RecommendationName = tostring(properties.description)\r\n | summarize Failed = countif(state == \"Unhealthy\"), Passed = countif(state == \"Healthy\" or complianceState == \"Passed\"), Total = countif(state == \"Unhealthy\" or state == \"Healthy\" or complianceState == \"Passed\") by RecommendationName, ControlID = controlId\r\n | extend PassedControls = (Passed/todouble(Total))*100\r\n| join kind = leftouter (securityresources\r\n | where type == \"microsoft.security/regulatorycompliancestandards/regulatorycompliancecontrols/regulatorycomplianceassessments\"\r\n | extend complianceStandardId = replace( \"-\", \" \", extract(@'/regulatoryComplianceStandards/([^/]*)', 1, id))\r\n | where complianceStandardId == \"Microsoft cloud security benchmark\"\r\n | extend RecommendationName = tostring(properties.description)\r\n | extend RecommendationLink = tostring(properties.assessmentDetailsLink)\r\n | project RecommendationName, RecommendationLink, name) on RecommendationName\r\n| extend ComplianceDomain=iff(ControlID contains \"AM.\", \"Asset Management\", iff(ControlID contains \"BR.\", \"Backup & Recovery\", iff(ControlID contains \"DP.\", \"Data Protection\", iff(ControlID contains \"DS.\", \"DevOps Security\", iff(ControlID contains \"ES.\", \"Endpoint Security\", iff(ControlID contains \"GS.\", \"Governance & Strategy\", iff(ControlID contains \"IM.\", \"Identity Management\", iff(ControlID contains \"IR.\", \"Incident Response\", iff(ControlID contains \"LT.\", \"Logging & Threat Detection\", iff(ControlID contains \"NS.\", \"Network Security\", iff(ControlID contains \"PA.\", \"Privileged Access\", iff(ControlID contains \"PV.\", \"Posture & Vulnerability Management\",\"Other\"))))))))))))\r\n| where ComplianceDomain in ({ComplianceDomain}) \r\n| extend Remediate=RecommendationLink\r\n| parse Remediate with * 'portal.azure.com/#blade/Microsoft_Azure_Security/RecommendationsBlade/assessmentKey/' assessmentKey '/' *\r\n| distinct RecommendationName, Total, Remediate, PassedControls, Passed, Failed, assessmentKey\r\n| sort by Total, Passed desc\r\n| limit 250",
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

In this query, Total is computed as countif(state == \"Unhealthy\" or state == \"Healthy\" or complianceState == \"Passed\") but does not include complianceState == \"Failed\". This can undercount the denominator and skew PassedControls for records where complianceState is populated (especially failures). Update the Total countif condition to include failed compliance states as well (aligning with how the other workbook query computes Total).

Copilot uses AI. Check for mistakes.
"query": "securityresources\r\n | where type == \"microsoft.security/regulatorycompliancestandards/regulatorycompliancecontrols/regulatorycomplianceassessments\"\r\n | extend complianceStandardId = replace( \"-\", \" \", extract(@'/regulatoryComplianceStandards/([^/]*)', 1, id))\r\n | where complianceStandardId == \"Microsoft cloud security benchmark\"\r\n | join kind = leftouter(\r\n securityresources\r\n | where type == \"microsoft.security/assessments\") on subscriptionId, name\r\n | extend complianceState = properties.state\r\n | extend resourceSource = tolower(tostring(properties1.resourceDetails.Source))\r\n | extend recommendationId = id1\r\n | extend resourceId = trim(' ', tolower(tostring(case(resourceSource =~ 'azure', properties1.resourceDetails.Id,\r\n resourceSource =~ 'gcp', properties1.resourceDetails.GcpResourceId,\r\n resourceSource =~ 'aws', properties1.resourceDetails.AwsResourceId,\r\n extract('^(.+)/providers/Microsoft.Security/assessments/.+$',1,recommendationId)))))\r\n | extend regexResourceId = extract_all(@\"/providers/[^/]+(?:/([^/]+)/[^/]+(?:/[^/]+/[^/]+)?)?/([^/]+)/([^/]+)$\", resourceId)[0]\r\n | extend resourceType = iff(regexResourceId[1] != \"\", regexResourceId[1], iff(regexResourceId[0] != \"\", regexResourceId[0], \"subscriptions\"))\r\n | extend resourceName = regexResourceId[2]\r\n | extend recommendationName = name\r\n | extend RecommendationName = properties1.displayName\r\n | extend description = properties1.metadata.description\r\n | extend remediationSteps = properties1.metadata.remediationDescription\r\n | extend severity = properties1.metadata.severity\r\n | extend state = properties1.status.code\r\n | extend notApplicableReason = properties1.status.cause\r\n | extend RecommendationLink = properties1.links.azurePortal\r\n | extend complianceStandardId = replace( \"-\", \" \", extract(@'/regulatoryComplianceStandards/([^/]*)', 1, id))\r\n | extend complianceControlId = extract(@\"/regulatoryComplianceControls/([^/]*)\", 1, id)\r\n | join kind = leftouter (securityresources\r\n | where type == \"microsoft.security/regulatorycompliancestandards/regulatorycompliancecontrols\"\r\n | extend complianceStandardId = replace( \"-\", \" \", extract(@'/regulatoryComplianceStandards/([^/]*)', 1, id))\r\n | where complianceStandardId == \"Microsoft cloud security benchmark\"\r\n | extend controlName = tostring(properties.description)\r\n | project controlId = name, controlName\r\n | distinct *) on $right.controlId == $left.complianceControlId\r\n | extend RecommendationName = tostring(properties.description)\r\n| join kind = leftouter (securityresources\r\n | where type == \"microsoft.security/regulatorycompliancestandards/regulatorycompliancecontrols/regulatorycomplianceassessments\"\r\n | extend complianceStandardId = replace( \"-\", \" \", extract(@'/regulatoryComplianceStandards/([^/]*)', 1, id))\r\n | where complianceStandardId == \"Microsoft cloud security benchmark\"\r\n | extend RecommendationName = tostring(properties.description)\r\n | extend RecommendationLink = tostring(properties.assessmentDetailsLink)\r\n | project RecommendationName, RecommendationLink) on RecommendationName\r\n| extend ComplianceDomain=iff(controlId contains \"AM.\", \"Asset Management\", iff(controlId contains \"BR.\", \"Backup & Recovery\", iff(controlId contains \"DP.\", \"Data Protection\", iff(controlId contains \"DS.\", \"DevOps Security\", iff(controlId contains \"ES.\", \"Endpoint Security\", iff(controlId contains \"GS.\", \"Governance & Strategy\", iff(controlId contains \"IM.\", \"Identity Management\", iff(controlId contains \"IR.\", \"Incident Response\", iff(controlId contains \"LT.\", \"Logging & Threat Detection\", iff(controlId contains \"NS.\", \"Network Security\", iff(controlId contains \"PA.\", \"Privileged Access\", iff(controlId contains \"PV.\", \"Posture & Vulnerability Management\",\"Other\"))))))))))))\r\n| where ComplianceDomain in ({ComplianceDomain}) \r\n | distinct RecommendationName, ComplianceDomain, tostring(RecommendationLink), tostring(state), tostring(complianceState)\r\n | summarize Failed = countif(state == \"Unhealthy\"), Passed = countif(state == \"Healthy\" or complianceState == \"Passed\"), Total = countif(state == \"Unhealthy\" or state == \"Healthy\" or complianceState == \"Passed\" or complianceState == \"Failed\") by ComplianceDomain\r\n | extend PassedControls = (Passed/todouble(Total))*100\r\n | project ControlFamily=ComplianceDomain, Total, PassedControls, Passed, Failed\r\n | sort by Total, Passed desc",
"size": 0,
"showAnalytics": true,
"title": "Recommendations by Control Area",
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

The PR description still includes the template 'Guidance <- remove section before submitting' block and placeholders for Change(s)/Reason/Testing/Validations. Please remove the guidance section and fill in the required PR fields so the intent and validation status of the workbook/template refresh is clear to reviewers.

Copilot uses AI. Check for mistakes.
@v-atulyadav v-atulyadav merged commit 15e2315 into master Apr 13, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Content-Package Solution Solution specialty review needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants