Update AzureSecurityBenchmark package and workbooks#14039
Update AzureSecurityBenchmark package and workbooks#14039v-atulyadav merged 3 commits intomasterfrom
Conversation
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.
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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).
| "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", |
There was a problem hiding this comment.
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.
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):
Reason for Change(s):
Version Updated:
Testing Completed:
Checked that the validations are passing and have addressed any issues that are present:
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.
Change(s):
Reason for Change(s):
Version updated:
Testing Completed:
Note: If updating a detection, you must update the version field.
Checked that the validations are passing and have addressed any issues that are present:
Note: Let us know if you have tried fixing the validation error and need help.