In our daily work me and my colleagues @Nixu write plenty of reports. This blog features project log for building full report automation for Microsoft’s excellent AZSK DevOps kit.
The report subject is company called Contoso (In the report ”Contoso Group”) People working with MS technologies might be aware of the company… no pun intended 🙂

Here is short demo of Contoso reports getting automated scan results from my throwaway MSDN environment
Background
AZSK DevOps kit is full security suite by Microsoft which they use also internally. If you are unfamiliar with it, I strongly recommend take a read here, my buddy Sami has made some nice things with it too here
Quick reference From the link: The Secure DevOps Kit for Azure (AzSK) was created by the Core Services Engineering & Operations (CSEO) division at Microsoft, to help accelerate Microsoft IT’s adoption of Azure. We have shared AzSK and its documentation with the community to provide guidance for rapidly scanning, deploying and operationalizing cloud resources, across the different stages of DevOps, while maintaining controls on security and governance.
AzSK is not an official Microsoft product – rather an attempt to share Microsoft CSEO’s best practices with the community..
Creating detailed report files from AZSK
It’s good to understand that AZSK provides plenty of reporting response to begin with (Export to PDF, .LOG and CSV by default) I’d suggest normally starting with these.
Due to extensive use of that output provided by AZSK we require further enrichment for optimal result.
Short rundown is that JSON, CSV, DOCX and .MD files are created with help of NodeJS parser which has templates and helper functions provided. These functions look up into additional definition file which’s existence was discovered by other colleague of mine. This was rather excellent finding 🙂
- Since the original report is table, I prefer report to be formatted as list as its better fit for anything even barely resembling official report styles
- Me and my colleagues have done also some grouping on AZSK side, to add all the resources to an array which helps with the list
- NodeJS generated .MD is also used to copy the report to DOCX
- This can also be automated by PANDOC, but due to word handily supporting an use of existing styling Copy/Paste actually works better.
- NodeJS already generates some additional information by fetching the rationales from the definition and creating styling for markdown
Once I am satisfied with the automation I will probably add the full NodeJS source files here, but for now, here are just some excerpts.
Some nice use cases:
- Adding control references to summaries in reports

- .MD files work out of the box for creating hierarchy on DOCX which can be conveniently browsed from the navigation view
- NodeJS generated .MD is is useful because its supported by many platforms such as Confluence and Azure Devops out of the box with use of existing widgets
- There is also existing AZSK solution for Azure DevOps (Maybe another blog, another Time)
Import-Module -Name AZSK
connect-AzAccount
$sub = (Get-AzSubscription | Out-GridView -PassThru).id
[string]$sub
Get-AzSKAzureServicesSecurityStatus -SubscriptionId $sub
$ReportFile = (Get-ChildItem -Path "$env:USERPROFILE\AppData\Local\Microsoft\AzSKLogs\" -Recurse | where {$_.FullName -match "CSV" -and $_.FullName -match "GRS" -and $_.FullName -notmatch "unsupported" -and $_.FullName -notmatch "Consolidated"}| Sort-Object -Property LastWriteTime -Descending | select -first 1).FullName
if (!$ReportFile) { Write-Host "no report file" -ForegroundColor red;Exit 0}
Write-Host "`nReport file path:`n$ReportFile" -ForegroundColor Green
$ReportCsv = Import-Csv $ReportFile
$GroupedList = $ReportCsv | Group-Object ControlID,Status,FeatureName,ControlSeverity,Description,Recommendation
# Consolidate
$Report = @()
ForEach ($Result in $GroupedList)
{
$PSObject = New-Object PSObject -Property @{
ControlID = $Result.Group[0].ControlId
Status = $Result.Group[0].Status
FeatureName = $Result.Group[0].FeatureName
ResourceName = $Result.Group.ResourceName
ControlSeverity = $Result.Group[0].ControlSeverity
Description = $Result.Group[0].Description
Recommendation = $Result.Group[0].Recommendation
}
$Report += $PSObject
}
$items = $Report.featurename | select -Unique
foreach ($service in $items) {
$sd = $report | where {$_.FeatureName -match $service }| sort-object -Property ControlSeverity | select controlid,*name*,reco*,*ControlSeverity*,status, *description* | ConvertTo-Json -Depth 2
$export =
'var data =' + $sd +
'
module.exports={data}'
mkdir .\md\ -force -confirm:$false
$export | Out-File .\md\export.js -Encoding utf8
node.exe .\azsk.js; start-sleep -Milliseconds 300
}
#
const fs = require('fs')
var chalk = require('chalk')
var {render} = require('mustache')
var {data} = require(__dirname + '/md/export')
var {getRationale} = require(__dirname + '/helpers')
var {template,topHeaders} = require('./template')
async function genRep () {
//
for (let index = 0; index < data.length; index++) {
const scannedItem = data[index]
let {FeatureName,ControlID} = scannedItem
if (index == 0) {
var createHeaders = render(topHeaders,scannedItem)
var addToexisting = true
try {fs.readFileSync(`${__dirname}\\md\\`+`00-results.md`) } catch {
console.log('creating new report file')
fs.writeFileSync(`${__dirname}\\md\\`+`00-results.md`,createHeaders)
addToexisting = false
console.log(addToexisting)
}
if (addToexisting == true) {
console.log('appending to existing report')
fs.appendFileSync(`${__dirname}\\md\\`+`00-results.md`,createHeaders)}
fs.writeFileSync(`${__dirname}\\md\\`+`00-results-${FeatureName}.md`,createHeaders)
}
var rationale = await getRationale (FeatureName,ControlID)
scannedItem.rationale = rationale
//console.log(data[index].FeatureName)
//console.log(scannedItem)
var output = render(template,scannedItem)
console.log(chalk.bold.yellowBright.bgGrey.bold(`creating report for${FeatureName}`,chalk.green.bgBlack(` ControlID: ${scannedItem.ControlID}`)))
fs.writeFileSync(`${__dirname}\\md\\` + `${scannedItem.ControlID}.md`, output)
fs.appendFileSync(`${__dirname}\\md\\`+`00-results-${FeatureName}.md`,output)
fs.appendFileSync(`${__dirname}\\md\\`+`00-results.md`,output)
}
}
genRep()
function stylizeContent (content,color,font,tags) {
if (tags) {
return `${tags.split(',')[0]}<span style= "font-family: ${font}; color: ${color}">${content}</span> ${tags.split(',')[1]}
`
} else {
return `<span style= "font-family: ${font}; color: ${color}">${content}</span> <br>
`
}
}
//let rationale = stylizeContent(' **Rationale**: {{rationale}}','black', 'Arial')
let ControlID = stylizeContent('{{ControlID}}','#0089cf', 'Arial',"<H4>,</H4>")
//console.log(rationale)
let Status = stylizeContent('**Status** {{Status}}','black', 'Arial',)
let Severity = stylizeContent('**Severity** {{ControlSeverity}}','black', 'Arial',)
let Recommendation = stylizeContent('**Recommendation** {{Recommendation}}','black', 'Arial',)
let Rationale = stylizeContent('**Rationale:** {{rationale}}','black', 'Arial',)
let Resources = stylizeContent(`**Resources**:{{#ResourceName}} <br> {{.}} {{/ResourceName}}`,'black', 'Arial',)
let Description = stylizeContent('**Description** {{Description}}','black', 'Arial',)
let template = `
${ControlID}
${Status}
${Severity}
${Recommendation}
${Rationale}
${Description}
${Resources}
`
let FeatureName = stylizeContent('{{FeatureName }}','#0089cf', 'Arial',"<H2>,</H2>")
let FeatureDescription = stylizeContent('This section describes the top recommendations and detailed audit findings on {{FeatureName}} resources.','black', 'Arial')
let TopRecommendations = stylizeContent('Top Recommendations','#0089cf', 'Arial',"<H3>,</H3>")
let TopControls = stylizeContent('Controls','#0089cf', 'Arial',"<H3>,</H3>")
let topHeaders = `
${FeatureName}
${FeatureDescription}
${TopRecommendations}
${TopControls}
`
module.exports={template,topHeaders}
To Be Continued 🙂
Br Joosua
Paluuviite: Project Log Part 3: Automating Azure Security Reports – Combining Subscription and resource security results – SecureCloudBlog
Paluuviite: Security Posture Management with Azure Policy and Microsoft Defender for Cloud – SecureCloudBlog