Monday, December 6, 2021

Powershell script to export certificate from Azure Keyvault

 # Replace these variables with your own values

$vaultName = "<key vault name>"

$certificateName = "<certificate name>"

$pfxPath = "<folder path>\<certname>.pfx"

$password = "<exportpassword>"

 

$cert = Get-AzKeyVaultCertificate -VaultName $vaultName -Name $certificateName

 

$pfxSecret = Get-AzKeyVaultSecret -VaultName $vaultName -Name $cert.Name -AsPlainText

 

 

$pfxUnprotectedBytes = [Convert]::FromBase64String($pfxSecret)

$pfx = New-Object Security.Cryptography.X509Certificates.X509Certificate2Collection

$pfx.Import($pfxUnprotectedBytes, $null, [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)

$pfxProtectedBytes = $pfx.Export([Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12, $password)

[IO.File]::WriteAllBytes($pfxPath, $pfxProtectedBytes)


Wednesday, November 10, 2021

Add VMSS extensions with settings and protected settings- Powershell

 The below script guides you to add/remove a vmss extension in Azure. The example is for adding Microsoft Monitoring Agent to Azure vmss which will have workspace Id to collect custom logs from VM's


Get VMSS to a variable

$vmssname= Get-AzVmss -ResourceGroupName npqatx -VMScaleSetName <vmssname>

$workspaceId="GUID of log analytics workspace"

 $workspaceKey="workspace key"

$settings=@{"workspaceId" = $workspaceId}

$protSettings=@{"workspaceKey" = $workspaceKey}

#Adding new extension

Add-AzVmssExtension -VirtualMachineScaleSet $vmssname -Name "MMAExtension" -Publisher "Microsoft.EnterpriseCloud.Monitoring" -Type "MicrosoftMonitoringAgent" -TypeHandlerVersion "1.0" -AutoUpgradeMinorVersion $true -Setting $settings -ProtectedSetting $protSettings

Update-AzVmss -ResourceGroupName "RGName" -VMScaleSetName $vmssname.Name -VirtualMachineScaleSet $vmssname


#Remove existing vmss extension

Remove-AzVmssExtension -VirtualMachineScaleSet $webvmss -Name "MMAExtension"

Update-AzVmss -ResourceGroupName "RGName" -VMScaleSetName $vmssname.Name -VirtualMachineScaleSet $vmssname


#make sure to upgrade instances in vmss to the latest model after every change to vmss.

Wednesday, October 27, 2021

Authenticate Azure APIM using Managed Identity to access Storage Account

<policies>
<inbound>
<!-- check the cache for secret first -->
<cache-lookup-value key="mysecret" variable-name="keyvaultsecretResponse" />
<!-- call Key Vault if not found in cache -->
<choose>
<when condition="@(!context.Variables.ContainsKey("keyvaultsecretResponse"))">
<send-request mode="new" response-variable-name="keyvaultsecret" timeout="20" ignore-error="false">
<set-url>https://msikvtest.vault.azure.net/secrets/mysecret/?api-version=7.0</set-url>
<set-method>GET</set-method>
<authentication-managed-identity resource="https://vault.azure.net" />
</send-request>
<!-- transform response to string and store in cache -->
<set-variable name="keyvaultsecretResponse" value="@(((IResponse)context.Variables["keyvaultsecret"]).Body.As<string>())" />
<cache-store-value key="mysecret" value="@((string)context.Variables["keyvaultsecretResponse"])" duration="3600" />
</when>
</choose>
<return-response>
<set-status code="200" reason="Done" />
<set-header name="content-type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@((string)context.Variables["keyvaultsecretResponse"])</set-body>
</return-response>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>

Monday, October 25, 2021

Azure Devops- Code Coverage report for typescript

 For running unit test, you can use Jasmine and the coverage report can be generated with nyc and report type as cobertura, which is supported in Azure DevOps.

Jasmine.json

{
    "spec_dir""test",
    "spec_files": ["**/*spec.ts"],
    "helpers": [
        "helpers/*.js"
    ]
 }

If you dont have any helper files, then you can remove the helpers section in above json file.

*.spec.ts file will have your test. Sample file below

describe('calculate', function() { it('add', function() { let result = Calculator.Sum(5, 2); expect(result).equal(7); }); });


Package.json

 "scripts": {
"test""ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json"
}


Now lets start the changes for code coverage.

First add an index.js file for cobertura report generation

'use strict'; /* Copyright 2012-2015, Yahoo Inc. Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ const path = require('path'); const { escape } = require('html-escaper'); const { ReportBase } = require('istanbul-lib-report'); class CoberturaReport extends ReportBase { constructor(opts) { super(); opts = opts || {}; this.cw = null; this.xml = null; this.timestamp = opts.timestamp || Date.now().toString(); this.projectRoot = opts.projectRoot || process.cwd(); this.file = opts.file || 'cobertura-coverage.xml'; } onStart(root, context) { this.cw = context.writer.writeFile(this.file); this.xml = context.getXMLWriter(this.cw); this.writeRootStats(root); } onEnd() { this.xml.closeAll(); this.cw.close(); } writeRootStats(node) { const metrics = node.getCoverageSummary(); this.cw.println('<?xml version="1.0" ?>'); this.cw.println( '<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">' ); this.xml.openTag('coverage', { 'lines-valid': metrics.lines.total, 'lines-covered': metrics.lines.covered, 'line-rate': metrics.lines.pct / 100.0, 'branches-valid': metrics.branches.total, 'branches-covered': metrics.branches.covered, 'branch-rate': metrics.branches.pct / 100.0, timestamp: this.timestamp, complexity: '0', version: '0.1' }); this.xml.openTag('sources'); this.xml.inlineTag('source', null, this.projectRoot); this.xml.closeTag('sources'); this.xml.openTag('packages'); } onSummary(node) { const metrics = node.getCoverageSummary(true); if (!metrics) { return; } this.xml.openTag('package', { name: node.isRoot() ? 'main' : escape(asJavaPackage(node)), 'line-rate': metrics.lines.pct / 100.0, 'branch-rate': metrics.branches.pct / 100.0 }); this.xml.openTag('classes'); } onSummaryEnd(node) { const metrics = node.getCoverageSummary(true); if (!metrics) { return; } this.xml.closeTag('classes'); this.xml.closeTag('package'); } onDetail(node) { const fileCoverage = node.getFileCoverage(); const metrics = node.getCoverageSummary(); const branchByLine = fileCoverage.getBranchCoverageByLine(); this.xml.openTag('class', { name: escape(asClassName(node)), filename: path.relative(this.projectRoot, fileCoverage.path), 'line-rate': metrics.lines.pct / 100.0, 'branch-rate': metrics.branches.pct / 100.0 }); this.xml.openTag('methods'); const fnMap = fileCoverage.fnMap; Object.entries(fnMap).forEach(([k, { name, decl }]) => { const hits = fileCoverage.f[k]; this.xml.openTag('method', { name: escape(name), hits, signature: '()V' //fake out a no-args void return }); this.xml.openTag('lines'); //Add the function definition line and hits so that jenkins cobertura plugin records method hits this.xml.inlineTag('line', { number: decl.start.line, hits }); this.xml.closeTag('lines'); this.xml.closeTag('method'); }); this.xml.closeTag('methods'); this.xml.openTag('lines'); const lines = fileCoverage.getLineCoverage(); Object.entries(lines).forEach(([k, hits]) => { const attrs = { number: k, hits, branch: 'false' }; const branchDetail = branchByLine[k]; if (branchDetail) { attrs.branch = true; attrs['condition-coverage'] = branchDetail.coverage + '% (' + branchDetail.covered + '/' + branchDetail.total + ')'; } this.xml.inlineTag('line', attrs); }); this.xml.closeTag('lines'); this.xml.closeTag('class'); } } function asJavaPackage(node) { return node .getRelativeName() .replace(/\//g, '.') .replace(/\\/g, '.') .replace(/\.$/, ''); } function asClassName(node) { return node.getRelativeName().replace(/.*[\\/]/, ''); } module.exports = CoberturaReport;


Add coverage script and index file reference in package.json

"main""index.js",
 "scripts": {
 "test""ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
 "coverage""nyc --reporter=lcov --reporter=text-lcov -r text -e .ts -x \"test/*.spec.ts\" npm run test"
}

"files": [
    "dist/**/*",
    "index.js"
  ]

"devDependencies": {

"jasmine""3.7.0",
"nyc""^15.1.0",
"ts-node""9.1.1",
"typescript""4.2.4"
}


Now coming to Azure devops pipeline.

Add the npm task for test script

taskNpm@1
          displayName'Run npm test'
          inputs:
            command'custom'
            workingDir'$(System.DefaultWorkingDirectory)/foldername'
            customCommand'run test'
            customRegistry'useFeed'
            customFeed'd1b0b66c-98e1-4731-aab9-7ba4e6c12630'
          conditionsucceededOrFailed()

For code coverage report, run the npm task by calling coverage script added in package

taskNpm@1
          displayName'Run npm coverage'
          inputs:
            command'custom'
            workingDir'$(System.DefaultWorkingDirectory)/foldername'
            customCommand'run coverage'
            customRegistry'useFeed'
            customFeed'd1b0b66c-98e1-4731-aab9-7ba4e6c12630'
          conditionsucceededOrFailed()
taskPublishCodeCoverageResults@1
          inputs:
            codeCoverageTool'Cobertura'
            summaryFileLocation'**/*cobertura-coverage.xml'


Thats it and you can see the code coverage report in your release output

Tuesday, October 19, 2021

Azure Runbook to add VMMS ID to KeyVault access policy

#Comment: Make sure you are not using Application ID parameter while adding access policy as it will add the identity as on behalf of.

# PowerShell code

########################################################

# Parameters

########################################################

[CmdletBinding()]

param(

    [Parameter(Mandatory=$True,Position=0)]

    [string]$NPResourceGroupName,

 

    [Parameter(Mandatory=$True,Position=1)]

    [string]$NPWebVmssID,

 

    [Parameter(Mandatory=$True,Position=2)]

    [string]$NPEngVmssID,

 

    [Parameter(Mandatory=$False,Position=3)]

    [string]$NPPayVmssID,


    [Parameter(Mandatory=$False,Position=4)]

    [string]$NPMasterKeyvaultName,


    [Parameter(Mandatory=$False,Position=5)]

    [string]$NPWebKeyvaultName

)

 

# Keep track of time

$StartDate=(GET-DATE)

 

 

 

########################################################

# Log in to Azure with AZ (standard code)

########################################################

Write-Verbose -Message 'Connecting to Azure'

  

# Name of the Azure Run As connection

$ConnectionName = 'AzureRunAsConnection'

try

{

    # Get the connection properties

    $ServicePrincipalConnection = Get-AutomationConnection -Name $ConnectionName      

   

    'Log in to Azure...'

    $null = Connect-AzAccount `

        -ServicePrincipal `

        -TenantId $ServicePrincipalConnection.TenantId `

        -ApplicationId $ServicePrincipalConnection.ApplicationId `

        -CertificateThumbprint $ServicePrincipalConnection.CertificateThumbprint 

}

catch 

{

    if (!$ServicePrincipalConnection)

    {

        # You forgot to turn on 'Create Azure Run As account' 

        $ErrorMessage = "Connection $ConnectionName not found."

        throw $ErrorMessage

    }

    else

    {

        # Something else went wrong

        Write-Error -Message $_.Exception.Message

        throw $_.Exception

    }

}


try

{


#Adding to master keyvault

Write-Verbose -Message 'Adding to master keyvault'


# Web vmss

$identityWeb = Get-AzUserAssignedIdentity -ResourceGroupName $NPResourceGroupName -Name $NPWebVmssID


Write-Verbose -Message 'Adding webvmssid'

'Adding webvmssid'

Set-AzKeyVaultAccessPolicy `

 -ResourceGroupName $NPResourceGroupName -VaultName $NPMasterKeyvaultName -ObjectId $identityWeb.PrincipalId `

 -PermissionsToKeys get,list,unwrapKey,wrapKey `

 -PermissionsToSecrets get -PermissionsToCertificates get,list,delete,create -BypassObjectIdValidation


# Eng vmss

$identityEng = Get-AzUserAssignedIdentity -ResourceGroupName $NPResourceGroupName -Name $NPEngVmssID


Write-Verbose -Message 'Adding engvmssid'

'Adding engvmssid'

Set-AzKeyVaultAccessPolicy `

 -ResourceGroupName $NPResourceGroupName -VaultName $NPMasterKeyvaultName -ObjectId $identityEng.PrincipalId `

 -PermissionsToKeys get,list,unwrapKey,wrapKey `

 -PermissionsToSecrets get -PermissionsToCertificates get,list,delete,create -BypassObjectIdValidation


# Pay vmss

$identityPay = Get-AzUserAssignedIdentity -ResourceGroupName $NPResourceGroupName -Name $NPPayVmssID


Write-Verbose -Message 'Adding payvmssid'

'Adding payvmssid'

Set-AzKeyVaultAccessPolicy `

 -ResourceGroupName $NPResourceGroupName -VaultName $NPMasterKeyvaultName -ObjectId $identityPay.PrincipalId `

 -PermissionsToKeys get,list,unwrapKey,wrapKey `

 -PermissionsToSecrets get -PermissionsToCertificates get,list,delete,create -BypassObjectIdValidation


#Adding to web keyvault

Write-Verbose -Message 'Adding to master keyvault'


# Web vmss

#$identityWeb = Get-AzUserAssignedIdentity -ResourceGroupName $NPResourceGroupName -Name $NPWebVmssID


Write-Verbose -Message 'Adding webvmssid'

'Adding webvmssid'

Set-AzKeyVaultAccessPolicy `

 -ResourceGroupName $NPResourceGroupName -VaultName $NPWebKeyvaultName -ObjectId $identityWeb.PrincipalId `

 -PermissionsToKeys get,list,unwrapKey,wrapKey `

 -PermissionsToSecrets get -PermissionsToCertificates get,list,delete,create -BypassObjectIdValidation


# Eng vmss

#$identityEng = Get-AzUserAssignedIdentity -ResourceGroupName $NPResourceGroupName -Name $NPEngVmssID


Write-Verbose -Message 'Adding engvmssid'

'Adding engvmssid'

Set-AzKeyVaultAccessPolicy `

 -ResourceGroupName $NPResourceGroupName -VaultName $NPWebKeyvaultName -ObjectId $identityEng.PrincipalId `

 -PermissionsToKeys get,list,unwrapKey,wrapKey `

 -PermissionsToSecrets get -PermissionsToCertificates get,list,delete,create -BypassObjectIdValidation


# Pay vmss

#$identityPay = Get-AzUserAssignedIdentity -ResourceGroupName $NPResourceGroupName -Name $NPPayVmssID


Write-Verbose -Message 'Adding payvmssid'

'Adding payvmssid'

Set-AzKeyVaultAccessPolicy `

 -ResourceGroupName $NPResourceGroupName -VaultName $NPWebKeyvaultName -ObjectId $identityPay.PrincipalId `

 -PermissionsToKeys get,list,unwrapKey,wrapKey `

 -PermissionsToSecrets get -PermissionsToCertificates get,list,delete,create -BypassObjectIdValidation


}

catch 

{

    Write-Error -Message $_.Exception.Message

    throw $_.Exception

}

Sunday, October 17, 2021

Powershell to update Azure DevOps pipeline variables automatically during the execution

 

Sometimes we have a scenario to update the devops pipeline variables dynamically during the task execution without having to update manually and create new release. you can use the below approach by adding Powershell task to the pipeline.

# Write your PowerShell commands here.

$cosmosconnstr = "$(cosmosconnstring)"

if("$(cosmosconnstring)".Chars("$(cosmosconnstring)".Length - 1) -eq ';')

{

  $cosmosconnstr = "$(cosmosconnstring)".TrimEnd(';')

}



Write-Output("##vso[task.setvariable variable=ApplicationSettings.CacheConnection;]$cosmosconnstr")


#End


#Run the below script in another stage to make sure you get the updated value

Write-host "CacheConnection Variable in previous task is: $(ApplicationSettings.CacheConnection)"

Azure Monitoring Agent extension for VMSS- Updating through ARM template

 You can add multiple extensions to Azure VMSS  through the extensionProfile of ARM template. The below script shows the extension configuration and mapping workspaceId to collect custom logs from VMSS instances.


{
                "name""AxMAExtension",
                "properties": {
                  "autoUpgradeMinorVersion"true,
                  "protectedSettings": {
                    "workspaceKey""[listKeys(resourceId(parameters('RGName'),'Microsoft.OperationalInsights/workspaces/', parameters('workspacename')),'2015-11-01-preview').primarySharedKey]"
                  },
                  "publisher""Microsoft.Azure.Monitor",
                  "settings": {
                    "workspaceId""[reference(resourceId(parameters('RGName'),'Microsoft.OperationalInsights/workspaces/', parameters('workspacename')), '2015-11-01-preview').customerId]"
                  },
                  "type""AzureMonitorWindowsAgent",
                  "typeHandlerVersion""1.0"
                }
              }

Wednesday, October 13, 2021

Custom dimensions and measurements - Azure Application Insights Query

 n App Analytics you can slice and dice on your App Insights custom dimensions and measurements just as easily as any of the so-called “standard” properties.

The only thing that’s a little bit tricky is extracting them first.

It’s tricky because of 2 things:

  1. You have to explicitly set the type of the measurement/dimension after you extract it.
  2. Extracting properties that contain spaces and special characters is a little bit of a hassle.
Bellow is an example

customEvents 
| where timestamp > ago(3h)
| where name == "Query"
| extend query_time = todouble(customMeasurements.['Query Time'])
| extend query_name = tostring(customDimensions.['Query Name'])
| project query_time, query_name
| summarize avg(query_time) by query_name 
| render barchart

User Enagagement metrix example

union *
| where timestamp > ago(90d)
| where client_Browser startswith "chrome" 
| evaluate activity_metrics(user_Id , timestamp, 7d, client_Browser   )
| where dcount_values > 3
| project timestamp , retention_rate, client_Browser 
| where retention_rate > 0 and 
  timestamp < ago(7d) and timestamp > ago(83d) // remove partial data in tail and head
| render timechart

Monday, September 20, 2021

TFS Build failure: folder cannot be deleted because it is not empty

When we run the build pipeline in Azure Devops, if we encounter below error then you can make the change recommended here.

Issue

D:\DevOps\Build\33\s\PT\<foldername>cannot be deleted because it is not empty. 

D:\DevOps\Build\33\s\PT\<foldername> cannot be deleted because it is not empty.

---- Summary: 0 conflicts, 1 warnings, 0 errors ---- 

Sleeping for 200 ms 

Retrying. Attempt $2/$3 

##[error]_proc should be null. (Parameter '_proc')


Cause

You can perform different kinds of cleaning of the working directory of your private agent before the build is run. If the Clean is set to false, it does not  get a fresh pull before the build is run.


Resolution


Change the clean dropdown to true and the error goes of in the next run.




Saturday, September 18, 2021

Update Azure SQL Database table using Service Principal Context

The script below checks whether a key exists in the DB and if yes it updates and if not it adds the key and value to the table. The context used here is the Service Principal Name (SPN) which is the client ID and secret key. Also the SQL authentication user ID and password is required. Make sure your machine ip is added in the firewall rules to run the query.

$dbuser = "username"

$password = "dbpswd"

$tenid = "tenantid"

$clientid = "client ID"

$secretkey = "Secret Key"

$Servername ="dbservername"

$database="dbname"


Write-Output "Starting"

#$clientid = Get-AzureRMAutomationVariable -Name $varclientid

#$secretkey = Get-AzureRMAutomationVariable -Name $varsecretkey

#$dbuser = Get-AzureRMAutomationVariable -Name $vardbuser

#$password = Get-AzureRMAutomationVariable -Name $vardbpass

#$sbpk = "test"


Add-SqlAzureAuthenticationContext -ClientID $clientid -Secret $secretkey -Tenant $tenid

$sqlConn = New-Object System.Data.SqlClient.SqlConnection

$sqlConn.ConnectionString = "Server=$Servername.database.windows.net; User ID = $dbuser ; Password = $password ; Database = $database; Column Encryption Setting=enabled;"

$sqlConn.Open()

Write-Output "sql conn opened"

function updateparamMaster($sqlconn,$paraID,$ParaGrp,$ParaValue)

{


#Check if the paramid and ParamGroup exists

$sqlcmd = New-Object System.Data.SqlClient.SqlCommand

$sqlcmd.Connection = $sqlConn

$query = "select * from parametermaster where paramid= $paraID and ParamGroup= '"+$ParaGrp+"'"

$sqlcmd.CommandText = $query

$adp = New-Object System.Data.SqlClient.SqlDataAdapter $sqlcmd

$data = New-Object System.Data.DataSet

$adp.Fill($data)

$paramcount = $data.Tables[0].Rows.count

Write-Host "Row count-" $paramcount


if($paramcount -eq 0)

{

Write-Output $ParaGrp "ParamGroup does not exist- creating new entry"

$sqlcmd = New-Object System.Data.SqlClient.SqlCommand

$sqlcmd.Connection = $sqlConn

$sqlcmd.CommandText = "INSERT into parametermaster(paramid,ParamGroup,Value) VALUES (@paramid, @ParamGroup, @Val)"

$sqlcmd.Parameters.Add((New-Object Data.SqlClient.SqlParameter("@paramid",[Data.SQLDBType]::Int)))

$sqlcmd.Parameters["@paramid"].Value = $paraID

$sqlcmd.Parameters.Add((New-Object Data.SqlClient.SqlParameter("@ParamGroup",[Data.SQLDBType]::VarChar, 50)))

$sqlcmd.Parameters["@ParamGroup"].Value = $ParaGrp

$sqlcmd.Parameters.Add((New-Object Data.SqlClient.SqlParameter("@val",[Data.SQLDBType]::NVarChar, 500)))

$sqlcmd.Parameters["@val"].Value =$ParaValue

$sqlcmd.ExecuteNonQuery();

}

else

{

Write-Output $ParaGrp "ParamGroup exist- updating entry"

$sqlcmd = New-Object System.Data.SqlClient.SqlCommand

$sqlcmd.Connection = $sqlConn

$sqlcmd.CommandText = "UPDATE parametermaster SET [Value] = @Val WHERE paramid = @paramid AND ParamGroup = @ParamGroup"

$sqlcmd.Parameters.Add((New-Object Data.SqlClient.SqlParameter("@paramid",[Data.SQLDBType]::Int)))

$sqlcmd.Parameters["@paramid"].Value = $paraID

$sqlcmd.Parameters.Add((New-Object Data.SqlClient.SqlParameter("@ParamGroup",[Data.SQLDBType]::VarChar, 50)))

$sqlcmd.Parameters["@ParamGroup"].Value = $ParaGrp

$sqlcmd.Parameters.Add((New-Object Data.SqlClient.SqlParameter("@val",[Data.SQLDBType]::NVarChar, 500)))

$sqlcmd.Parameters["@val"].Value =$ParaValue

$sqlcmd.ExecuteNonQuery();

}


}


updateparamMaster -sqlconn $sqlConn -paraID 118 -ParaGrp "SharedDb" -ParaValue