Deploying Web Jobs Using ARM or the Powershell Script - c#

I am trying to deploy Web job in to Azure using powershell script where we have our own deployment Framework, when i am deploying from my local machine it is deploying successfully only if i have the Admin or Co- Admin permissions if i have contributor Access script fails.Our deployment framework has only contributor permissions, Is there any way to deploy web job with the contributor permissions and I didn't found any ARM templates for Web Job deployment where i have solution file in local . Any suggestions please
Here is my script
`Add-AzureAccount (since web job doesn't have any RM commandlett i am using classic login where All our resource are in RM )
Select-AzureSubscription -SubscriptionName "*************"
$packageSource ="$Global:packageDir/../output\deletelogs\bin"
$destinationSource = "$env:TEMP\DeleteLogsWebjob.zip"
If(Test-path $destinationSource) {Remove-item $destinationSource}
Add-Type -assembly "system.io.compression.filesystem"
[io.compression.zipfile]::CreateFromDirectory($packageSource, $destinationSource)
$webappname = AzCreateWebAppName "mywebapp$(Get-Random)"
$location = "EastUs" #GetCurrentRegion
$resourceGroupName= "*****" # AzGetResourceGroup
$webjobName = $webappname +"_deletelogsjob"
$jobCollecctionName = $webappname + $webjobName + "JobCollection"
#<### Defining Schedules
#$date = Get-Date
#$startDate = $date.ToString("MM-dd-yyy HH:mm tt")
#$endDate = $date.AddYears(1).ToString("MM-dd-yyy HH:mm tt")#>
# #Create an App Service plan in Free tier.
New-AzureRmAppServicePlan -Name $webappname -Location $location -ResourceGroupName $resourceGroupName -Tier Basic
## Create a web app.
$webapp = New-AzureRmWebApp -Name $webappname -Location $location -AppServicePlan $webappname -ResourceGroupName $resourceGroupName
###### Create WebJob
$job = New-AzureWebsiteJob -Name $webapp -JobName $webjobName -JobType Triggered -JobFile $destinationSource
$jobCollection = New-AzureRmSchedulerJobCollection -Location $location -JobCollectionName $jobCollecctionName -ResourceGroupName $resourceGroupName
$temp = "$env:TEMP\appsetting.xml"
$file = Get-AzureRMWebAppPublishingProfile -ResourceGroupName $resourceGroupName -Name $webappname -OutputFile $temp -Format WebDeploy
$webSitePassword = ([xml]$file).ChildNodes[0].ChildNodes[0].Attributes[5].Value
$webSiteUserName = ([xml]$file).ChildNodes[0].ChildNodes[0].Attributes[4].Value
$uri = "https://{0}:{1}#{2}.scm.azurewebsites.net/api/triggeredwebjobs/{3}/run" -f $webSiteUserName, $webSitePassword,$webappname, $webjobName
New-AzureRmSchedulerHttpJob -ResourceGroupName $resourceGroupName `
-JobCollectionName $jobCollection[0].JobCollectionName -JobName "deleteLogsScheduler" -Method POST `
-URI $uri -StartTime $startDate -Interval 2 -Frequency Minute `
-EndTime $endDate`

You could use example in this blog.
#Resource details :
$resourceGroupName = "<Resourcegroup name>";
$webAppName = "<WebApp name>";
$Apiversion = 2015-08-01
#Function to get Publishing credentials for the WebApp :
function Get-PublishingProfileCredentials($resourceGroupName, $webAppName){
$resourceType = "Microsoft.Web/sites/config"
$resourceName = "$webAppName/publishingcredentials"
$publishingCredentials = Invoke-AzureRmResourceAction -ResourceGroupName $resourceGroupName -ResourceType
$resourceType -ResourceName $resourceName -Action list -ApiVersion $Apiversion -Force
return $publishingCredentials
}
#Pulling authorization access token :
function Get-KuduApiAuthorisationHeaderValue($resourceGroupName, $webAppName){
$publishingCredentials = Get-PublishingProfileCredentials $resourceGroupName $webAppName
return ("Basic {0}" -f [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f
$publishingCredentials.Properties.PublishingUserName, $publishingCredentials.Properties.PublishingPassword))))
}
$accessToken = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $webAppname
#Generating header to create and publish the Webjob :
$Header = #{
'Content-Disposition'='attachment; attachment; filename=Copy.zip'
'Authorization'=$accessToken
}
$apiUrl = "https://$webAppName.scm.azurewebsites.net/api/<Webjob-type>/<Webjob-name>"
$result = Invoke-RestMethod -Uri $apiUrl -Headers $Header -Method put -InFile "<Complete path of the file>\
<filename>.zip" -ContentType 'application/zip'
#NOTE: Update the above script with the parameters highlighted and run in order to push a new Webjob under the specified WebApp.
For Kudu authorization
$creds = Invoke-AzureRmResourceAction -ResourceGroupName YourResourceGroup -ResourceType Microsoft.Web/sites/config -ResourceName YourWebApp/publishingcredentials -Action list -ApiVersion 2015-08-01 -Force
$username = $creds.Properties.PublishingUserName
$password = $creds.Properties.PublishingPassword
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username,$password)))
$apiBaseUrl = "https://$($website.Name).scm.azurewebsites.net/api"
$kuduVersion = Invoke-RestMethod -Uri "$apiBaseUrl/environment" -Headers #{Authorization=("Basic {0}" -f $base64AuthInfo)} -Method GET

This following lines of code is power shell script which allows you to deploy webjob in a web app . The parameters are passed through the settings file reference ...
#Requires -Version 3.0
Param(
[string] $settingsFileName = '\Settings\settings.json',
[boolean] $unitTestMode = $false
)
$Apiversion = "2015-08-01"
# Define FUNCTIONS
#Check if any requirement is missing
function Test-ParameterSet
{
param
(
[parameter(Mandatory = $true)][System.Object]$settings
)
# Loop through the $settings and check no missing setting
if ($null -eq $applicationFileJson.subscriptions) {throw "Missing subscriptions field in settings file"}
foreach ($subscription in $applicationFileJson.subscriptions)
{
if ($null -eq $subscription.subscriptionId) {throw "Missing subscription Id field in settings file"}
# Loop through the $settings and check no missing setting
foreach ($vault in $settings.WorkLoads)
{
if ($null -eq $vault.applicationName) {throw "Missing applicationNam in settings file for $($subscription.subscriptionId)"}
if ($null -eq $vault.environmentName) {throw "Missing environmentName in settings file for $($subscription.subscriptionId)"}
if ($null -eq $vault.resourceGroupName) {throw "Missing resourceGroupName in settings file for $($subscription.subscriptionId)"}
if ($null -eq $vault.webAppName) {throw "Missing webAppName in settings file for $($subscription.subscriptionId)"}
if ($null -eq $vault.Storagerg) {throw "Missing Storagerg in settings file for $($subscription.subscriptionId)"}
if ($null -eq $vault.webjobname) {throw "Missing webjobname in settings file for $($subscription.subscriptionId)"}
if ($null -eq $vault.webjobtype) {throw "Missing webjobtype in settings file for $($subscription.subscriptionId)"}
if ($null -eq $vault.storageAccountName) {throw "Missing storageAccountName in settings file for $($subscription.subscriptionId)"}
if ($null -eq $vault.container_name) {throw "Missing container_name in settings file for $($subscription.subscriptionId)"}
If ($vault.webjobtype -eq "triggeredwebjobs")
{
if ($null -eq $vault.trigger_type) {throw "Missing trigger_type in settings file for $($subscription.subscriptionId)"}
}
}
If ($vault.webjobtype -eq "triggeredwebjobs" -and $vault.trigger_type -eq "scheduled")
{
if ($null -eq $vault.schedule) {throw "Missing schedule in settings file for $($subscription.subscriptionId)"}
If ($vault.webjobtype -eq "continuouswebjobs")
{
if ($null -eq $vault.continuous_type) {throw "Missing continuous_type in settings file for $($subscription.subscriptionId)"}
}
}
return $true
}
}
function Get-KuduApiAuthorisationHeaderValue($resourceGroupName, $webAppName)
{
$resourceType = "Microsoft.Web/sites/config"
$resourceName = "$webAppName/publishingcredentials"
$publishingCredentials = Invoke-AzureRmResourceAction -ResourceGroupName $resourceGroupName -ResourceType $resourceType -ResourceName $resourceName -Action list -ApiVersion $Apiversion -Force
return ("Basic {0}" -f [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $publishingCredentials.Properties.PublishingUserName, $publishingCredentials.Properties.PublishingPassword))))
}
function Publish-webjob
{
[OutputType([String])]
param
(
[parameter(Mandatory = $true)][string]$resourceGroupName,
[parameter(Mandatory = $true)][string]$webAppName,
[parameter(Mandatory = $true)][string]$Storagerg,
[parameter(Mandatory = $true)][string]$webjobname,
[parameter(Mandatory = $true)][string]$webjobtype,
[parameter(Mandatory = $true)][string]$storageAccountName,
[parameter(Mandatory = $true)][string]$container_name,
[parameter(Mandatory = $true)][string]$settingsFileName,
[parameter(Mandatory = $true)][string]$i
)
#Check resource group exist
try {
$resourceGroup = Get-AzureRmResourceGroup -Name $resourceGroupName -ErrorAction Stop
}
catch {
$resourceGroup = $null
}
if ($null -eq $resourceGroup)
{
$message = "Resource group $resourceGroupName not found, deployment stop"
Write-Verbose $message
return $message
}
else
{
# Prepare deployment variables
Write-Verbose "ResourceGroup Found"
$SettingsJson = Get-JsonParameterSet -settingsFileName $settingsFileName
$trigger_type = "$($SettingsJson.WorkLoads[$i].trigger_type)"
Write-Verbose $trigger_type
$schedule = "$($SettingsJson.WorkLoads[$i].schedule)"
Write-Verbose $schedule
$continuous_type = "$($SettingsJson.WorkLoads[$i].continuous_type)"
Write-Verbose $continuous_type
}
# Unlock ResourceGroup
Unlock-ResourceGroup $resourceGroupName
write-verbose "ResourceGroup Unlocked"
$accessToken = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $webAppname
#Get storage account context and storage bolo
$sa= Get-AzureRmStorageAccount -ResourceGroupName $Storagerg -Name $storageAccountName
$ctx=$sa.Context
$blobs = Get-AzureStorageBlob -Container $container_name -Context $ctx
$webjobtype=$webjobtype+"webjobs"
$apiUrl = "https://$webAppName.scm.azurewebsites.net/api/$webjobtype/$webjobname"
#create a folder to save all the files of the container
$Location=$PSScriptRoot
New-Item -Path $Location -Name $webjobname -ItemType "directory" -Force
$folderPath = "$Location"+"\"+$webjobname
$localFile = $Location+"\"+$webjobname+"\"
#Generating header to create and publish the Webjob :
$Header = #{
"Content-Disposition"="attachment;filename=$($webAppName)"
"Authorization"=$accessToken
}
#Check if the storage container is empty
If($blobs.Count -eq 0)
{
Write-Error "The storage container is found empty"
}
#get files from container
foreach ($blob in $blobs)
{
$file=New-TemporaryFile
$file=Get-AzureStorageBlobContent -Container $container_name -Blob $blob.Name -Context $ctx -Destination $localFile -Force
$contents = Get-Content $localFile -Raw -ErrorAction:SilentlyContinue
$f=New-TemporaryFile
Add-Content $f $contents
}
If ($webjobtype -eq "triggeredwebjobs" -and $trigger_type -eq "scheduled")
{
$f = New-TemporaryFile
$r="{ ""schedule"" : "+""""+ $schedule+"""" + "}"
Add-Content $f $r
$destination=$Location+"\"+$webjobname+"\settings.job"
Move-Item -Path $f -Destination $destination
}
If ($webjobtype -eq "continuouswebjobs")
{
$f = New-TemporaryFile
If($continuous_type -eq "singleinstance")
{
$c="true"
}
If($continuous_type -eq "multipleinstance")
{
$c="false"
}
$r="{ ""is_singleton"" : " + $c + "}"
Add-Content $f $r
$destination=$Location+"\"+$webjobname+"\settings.job"
Move-Item -Path $f -Destination $destination
}
#Archive the files to a zip
$source = $localFile + "*"
$wz=$webjobname+".zip"
$destination = $Location+"\"+$webjobname+"\"+$wz
Compress-Archive -U -Path $source -DestinationPath $destination
#Deploying webjobs
$result = Invoke-RestMethod -Uri $apiUrl -Headers $Header -Method put -InFile $destination -ContentType 'application/zip' -ErrorAction Stop
Write-Host "Print result " -ForegroundColor Green
#delete the folder from local mahine
Remove-Item $folderPath -Force -Recurse -ErrorAction SilentlyContinue
# lock ResourceGroup
#Lock-ResourceGroup $resourceGroupName
write-verbose "ResourceGroup locked"
return $result
}
function Publish-Infrastructure
{
param(
[parameter(Mandatory = $true)][string]$settingsFileName
)
$settings = Get-JsonParameterSet -settingsFileName $settingsFileName
$deploymentIsSucceeded = $true
$workloadCount = $settings.WorkLoads.Count
Write-Verbose "workloadCounts: $workloadCount"
if($workloadCount -ge 1)
{
for($i = 0;$i -lt $workloadCount; $i++)
{
$applicationName = $settings.WorkLoads[$i].applicationName
Write-Verbose "application name: $applicationName"
$environmentName = $settings.WorkLoads[$i].environmentName
$applicationFile = "..\SettingsByWorkload\" + "nv_" + $applicationName + ".workload.json"
$applicationFile = Get-FileFullPath -fileName $applicationFile -rootPath $PSScriptRoot
$applicationFileJson = Get-JsonParameterSet -settingsFileName $applicationFile
Write-Verbose "application file json: $applicationName"
$null = Test-ParameterSet -settings $settings
$policyCount = $applicationFileJson.subscriptions.Count
Write-Verbose "$policyCount"
if($policyCount -ge 1)
{
for($j = 0;$j -lt $policyCount; $j++)
{
if($applicationFileJson.subscriptions[$j].environmentName -eq $environmentName)
{
$subscriptionId = $applicationFileJson.subscriptions[$j].subscriptionId
Write-Verbose "Environment Subscription: $($subscriptionId)"
Set-ContextIfNeeded -SubscriptionId $subscriptionId
foreach ($webjob in $settings.WorkLoads[$i])
{
$resourceGroupName = $webjob.resourceGroupName
$webAppName = $webjob.webAppName
$Storagerg = $webjob.Storagerg
$webjobname = $webjob.webjobname
$webjobtype = $webjob.webjobtype
$storageAccountName = $webjob.storageAccountName
$schedule = $webjob.schedule
$container_name = $webjob.container_name
$trigger_type = $webjob.trigger_type
Write-Verbose "Ready to start deployment on environment $EnvironmentName of a webjob in subscription $subscriptionId for resource group: $resourceGroupName"
$result = Publish-webjob ` -resourceGroupName $resourceGroupName `
-webAppName $webAppName `
-Storagerg $Storagerg `
-webjobname $webjobname `
-webjobtype $webjobtype `
-storageAccountName $storageAccountName `
-settingsFileName $settingsFileName `
-container_name $container_name `
-i $i
}
}
}
}
}
}
return $true
}
#START OF SCRIPT
if ($unitTestMode)
{
#do nothing
Write-Verbose 'Unit test mode, no deployment' -Verbose
}
else
{
#Log in Azure if not already done
try
{
$azureRmContext = Get-AzureRmContext -ErrorAction Stop
}
catch
{
$result = Add-AzureRmAccount
$azureRmContext = $result.Context
}
Write-Verbose "Subscription name $($azureRmContext.Subscription.Name)" -Verbose
$VerbosePreference = 'Continue'
# Get required templates and setting files. Throw if not found
$scriptsPath=$PSScriptRoot
$scriptsPath = Split-Path -Path $scriptsPath -Parent
$SettingsPath = Join-Path $scriptsPath $settingsFileName
$settingsFileName = $SettingsPath
Write-Verbose "Settings file name $($settingsFileName)" -Verbose
# Deploy infrastructure
return Publish-Infrastructure `
-settingsFileName $settingsFileName `
# END of script
}

Related

powershell net view not showing all computer names on network

Using the one-liner
switch -regex (NET.EXE VIEW) { "^\\\\(?<Name>\S+)\s+" { $matches.Name } }
To get a list of all computers on the network, but for some reason, this only returns a partial list, could it be a work-group related issue?
I can see all the computers on the network under the windows network tab and log in successfully, though.
Made a bit of a script for this that grabs all of the network computers via arp -a, hostnames etc, with a progress bar, and then feeds them to an array that I can use.
Add-Type -assembly System.Windows.Forms
## -- Create The Progress-Bar
$ObjForm = New-Object System.Windows.Forms.Form
$ObjForm.Text = "Initial Setup"
$ObjForm.Height = 100
$ObjForm.Width = 500
$ObjForm.BackColor = "#000000"
$ObjForm.FormBorderStyle = 'None'
$ObjForm.ControlBox = $False
$ObjForm.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedSingle
$ObjForm.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
$ObjForm.TopMost = $True
## -- Create The Label
$ObjLabel = New-Object System.Windows.Forms.Label
$ObjLabel.Text = "Starting. Please wait ... "
$ObjLabel.Left = 5
$ObjLabel.Top = 10
$ObjLabel.Width = 500 - 20
$ObjLabel.Height = 15
$ObjLabel.Font = "Tahoma"
$ObjLabel.ForeColor = "#FFFFFF"
## -- Add the label to the Form
$ObjForm.Controls.Add($ObjLabel)
$PB = New-Object System.Windows.Forms.ProgressBar
$PB.Name = "PowerShellProgressBar"
$PB.Value = 0
$PB.Style = "Continuous"
$PB.ForeColor = "#FFC20E"
$PB.BackColor = "#111111"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 500 - 40
$System_Drawing_Size.Height = 20
$PB.Size = $System_Drawing_Size
$PB.Left = 5
$PB.Top = 40
$ObjForm.Controls.Add($PB)
## -- Show the Progress-Bar and Start The PowerShell Script
$ObjForm.Show() | Out-Null
$ObjForm.Focus() | Out-NUll
$ObjLabel.Text = "Starting. Please wait ... "
$ObjForm.Refresh()
Start-Sleep -Seconds 1
## -- Execute The PowerShell Code and Update the Status of the Progress-Bar
$Activity = "Processing items"
$NameArray = arp -a
$ArpArray = #()
$FinalArray = #()
foreach ($object in $NameArray)
{
$ArpArray += ($object -split "\s+")[1] -replace "Internet", ""
}
$TotItems = $ArpArray.Count
$Count = 0
$ParseArray = $ArpArray | Where { $_ -ne '' }
foreach ($address in $ParseArray)
{
#write-host $address
$Stringer = $address
try { $Name = [System.Net.Dns]::GetHostByAddress($Stringer).Hostname }
catch [Exception]{ $Name = "" }
$FinalArray += $Name
$Count++
$percentComplete = ($Count/$TotItems * 100)
$PB.Value = $percentComplete
$ObjLabel.Text = "Getting Network Computer Names"
$ObjForm.Refresh()
Start-Sleep -Milliseconds 150
}
$FinalArray = $FinalArray | Where { $_ -ne "" }
$FinalArray = $FinalArray | Select -Unique
#$FinalArray
$ObjForm.Close()

Call Powershell function from C#

Hi to all I want to call in C# a powershell script. Inside ps1 file I have implemented a function.
Powershell script:
Add-Type -path 'C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.Runtime.dll'
Add-Type -path 'C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.dll'
#$CourseName = Read-host "Enter site name"
CreateBlogSubsite($SiteName)
Function CreateBlogSubsite($SiteName)
{
$user = "johndoe#tenant.onmicrosoft.com";
$pass = "P3003ksi434!";
$secpw = $pass | ConvertTo-SecureString -AsPlainText -Force
$SiteURL = "https://tenant.sharepoint.com/sites/SalesSite/"
$Context = New-Object Microsoft.SharePoint.Client.ClientContext($SiteURL);
$Creds = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($user,$secpw);
$Context.Credentials = $Creds;
Try {
#Specify Subsite details
$WebCI = New-Object Microsoft.SharePoint.Client.WebCreationInformation
$WebCI.Title = $SiteName + " Blog" # sitename
$WebCI.WebTemplate = "Blog#0" #Blog Site #site template
$WebCI.Url = $SiteName + "_Blog"
$SubWeb = $Context.Web.Webs.Add($WebCI)
$Context.ExecuteQuery()
$URI = $SiteURL + $WebCI.Url
return $URI
#Write-host "Subsite Created Successfully! Url is: " + $SiteName + "1Blog" -ForegroundColor Green
}
catch {
write-host "Error: $($_.Exception.Message)" -foregroundcolor Red
}
}
Here is my console program, where I call PS script:
static void Main(string[] args)
{
// Execute powershell script
// Initialize PowerShell engine
var shell = PowerShell.Create();
//Add the script via a pre-made ps1 file
shell.Commands.AddScript(#"C:\\Users\\zeb\\source\\CreateCourseBlog.ps1");
shell.Commands.AddParameter("$SiteName", "Desti");
// Execute the script
var results = shell.Invoke(); // I want to get output of Poweshell function
Console.Write(results);
}
But it does not works :( . So, does not create subsite when I call script from c#
This should work:
Runspace rs = RunspaceFactory.CreateRunspace();
rs.Open();
using (PowerShell ps = PowerShell.Create())
{
ps.Runspace = rs;
ps.AddScript($#"C:\Users\zeb\source\CreateCourseBlog.ps1 -SiteName Desti");
ps.AddCommand("Out-String");
var psOutput = ps.Invoke();
foreach (var item in psOutput)
{
if (item == null) continue;
Console.WriteLine(item.BaseObject.ToString());
}
if (ps.Streams.Error.Count > 0)
Console.WriteLine($"Errors ({ps.Streams.Error.Count}):\n");
foreach (var err in ps.Streams.Error)
Console.WriteLine(" - " + err);
}
In addition to this code you should add next code to the top of your powershell script:
Param(
[string] $SiteName
)

How can I identify duplicate application manifest certificates

I have successfully:
Created an Azure App Service
Registered it with AAD
Added a certificate to the application manifest using New-AzureADApplicationKeyCredential.
The customKeyIdentifier is set as follows:
[Convert]::ToBase64String($global:CertificateInfo.Certificate.GetCertHash()) ;
Accessed the App Service using the certificate to get an access token via AcquireTokenAsync.
While attempting to write Powershell to make it easier to add certificates to applications, I noticed that you can add an unlimited number of duplicate certificates. What I mean is that they all have a unique keyId but the same
customKeyIdentifier. So I wrote the following code to eliminate creating duplicates
if ($global:CertificateInfo.Certificate -eq $null)
{
throw "No certificate has been selected or created yet."
}
$filter = "DisplayName eq '" + $($DisplayName) + "'" ;
$global:CertificateInfo.Application = Get-AzureADApplication -filter $filter
$certificateThumbprint = [System.Convert]::ToBase64String($global:CertificateInfo.Certificate.GetCertHash()) ;
foreach($keyCredential in $global:CertificateInfo.Application.KeyCredentials)
{
[String]$keyCredentialThumbPrint = [System.Convert]::ToBase64String($keyCredential.CustomkeyIdentifier) ;
if([String]::Equals($keyCredentialThumbPrint,$certificateThumbprint,[StringComparison]::CurrentCultureIgnoreCase)) {
throw "This certificate already exists within the keyCredentials collection with KeyId" + "'" + $keyCredential.KeyId + "'" ;
}
}
The code does not work because the customKeyIdentifier saved in the application manifest is modified somehow by Azure when it is saved so my duplication check fails. Does anyone know how Azure is modifying the customKeyIdentifier so that I can get my duplicate check to work?
Below is a copy of the duplicates that can show up in your application manifest
"keyCredentials": [
{
"customKeyIdentifier": "N0l6V0gxM3phNGxvUUk2UnZNdFE0dWV3aDFnPQ==",
"endDate": "2019-12-18T19:22:10Z",
"keyId": "6bef2fd1-b163-44fd-8f70-90828a6003ef",
"startDate": "2017-12-18T23:05:28.4976081Z",
"type": "AsymmetricX509Cert",
"usage": "Verify",
"value": null
},
{
"customKeyIdentifier": "N0l6V0gxM3phNGxvUUk2UnZNdFE0dWV3aDFnPQ==",
"endDate": "2019-12-18T19:22:10Z",
"keyId": "d73d0903-d86f-4277-bbe9-e1cea078b400",
"startDate": "2017-12-18T21:30:05.8419846Z",
"type": "AsymmetricX509Cert",
"usage": "Verify",
"value": null
}
So that people better understand that the issue is in using the New-AzureADApplicationKeyCredential cmdlet and not in the duplicate compare logic I have included the Powershell code I am using below
$global:CertificateInfo = #{} ;
function Connect-Azure {
[CmdletBinding()]
param
(
[parameter(Mandatory=$true)]
[string] $TenantId
)
Write-Host "Connect-Azure - Enter - $($MyInvocation.MyCommand.Name)"
Write-Host "Get-ApplicatonKeyCredentials - Parameters"
Write-Host " TenantId - $($TenantId)"
$ErrorActionPreference = 'Stop';
Connect-AzureAD -TenantId $TenantId
Write-Host "Connect-Azure - Exit - $($MyInvocation.MyCommand.Name)"
}
function Add-AzureADApplicationKeyCredential {
[CmdletBinding()]
param
(
[parameter(Mandatory=$true)]
[string] $DisplayName,
[parameter(Mandatory=$false)]
[Switch] $Force
)
Write-Host "Add-AzureADApplicationKeyCredential - Enter - $($MyInvocation.MyCommand.Name)"
Write-Host "Add-AzureADApplicationKeyCredential - Parameters"
Write-Host " DisplayName - $($DisplayName)"
$ErrorActionPreference = 'Stop';
if ($global:CertificateInfo.Certificate -eq $null)
{
throw "No certificate has been selected or created yet."
}
$filter = "DisplayName eq '" + $($DisplayName) + "'" ;
$global:CertificateInfo.Application = Get-AzureADApplication -filter $filter
$certificateThumbprint = [System.Convert]::ToBase64String($global:CertificateInfo.Certificate.GetCertHash()) ;
foreach($keyCredential in $global:CertificateInfo.Application.KeyCredentials)
{
[String]$keyCredentialThumbPrint = [System.Convert]::ToBase64String($keyCredential.CustomkeyIdentifier) ;
if([String]::Equals($keyCredentialThumbPrint,$certificateThumbprint,[StringComparison]::CurrentCultureIgnoreCase)) {
throw "This certificate already exists within the keyCredentials collection with KeyId" + "'" + $keyCredential.KeyId + "'" ;
}
}
$CertificateInfo = #{} ;
$CertificateInfo.CustomKeyIdentifier = [Convert]::ToBase64String($global:CertificateInfo.Certificate.GetCertHash()) ;
$CertificateInfo.Value = [System.Convert]::ToBase64String($global:CertificateInfo.Certificate.GetRawCertData()) ;
$CertificateInfo.EndDate = $global:CertificateInfo.Certificate.NotAfter ;
$CertificateInfo.Type = "AsymmetricX509Cert"
$CertificateInfo.Usage = "Verify" ;
$CertificateInfo.ObjectId = $global:CertificateInfo.Application.ObjectId ;
New-AzureADApplicationKeyCredential #CertificateInfo;
Write-Host "Add-AzureADApplicationKeyCredential - Exit"
}
function Select-Certificate {
[CmdletBinding()]
param
(
[parameter(Mandatory=$false)]
[string] $CertStoreLocation = "Cert:\LocalMachine\My",
[parameter(Mandatory=$true)]
[string] $ThumbPrint
)
Write-Host "Create-SelfSignedCertificate - Enter - $($MyInvocation.MyCommand.Name)"
Write-Host "Get-ApplicatonKeyCredentials - Parameters" $ErrorActionPreference = 'Stop';
$certificateLocation = $CertStoreLocation + "\" + $ThumbPrint ;
$global:CertificateInfo.Certificate = (Get-ChildItem –Path "$($certificateLocation)")
}
function Create-SelfSignedCertificate {
[CmdletBinding()]
param
(
[parameter(Mandatory=$false)]
[string] $Subject,
[parameter(Mandatory=$false)]
[string] $HashAlgorithm = "SHA256",
[parameter(Mandatory=$false)]
[string] $CertStoreLocation = "Cert:\LocalMachine\My",
[parameter(Mandatory=$true)]
$NotAfter
)
## see https://blogs.technet.microsoft.com/scotts-it-blog/2014/12/30/working-with-certificates-in-powershell/
Write-Host "Create-SelfSignedCertificate - Enter - $($MyInvocation.MyCommand.Name)"
Write-Host "Get-ApplicatonKeyCredentials - Parameters"
if([string]::IsNullOrEmpty($Subject)) {
$currentDate = (Get-Date) ;
$Subject = [String]::Format("SelfSigned{0:yyyymmddHHMMss}",$currentDate) ;
}
Write-Host " Subject - $($Subject)"
Write-Host " HashAlgorithm - $($HashAlgorithm)"
Write-Host " CertStoreLocation - $($CertStoreLocation)"
Write-Host " NotAfter - $($NotAfter)"
$ErrorActionPreference = 'Stop';
Write-Host "Create-SelfSignedCertificate - Exit - $($MyInvocation.MyCommand.Name)"
$SaveChooser = New-Object -Typename System.Windows.Forms.SaveFileDialog
$SaveChooser.CreatePrompt = $false ;
$SaveChooser.Title = "Save certficate" ;
$SaveChooser.DefaultExt = "pfx" ;
$dialogResult = $SaveChooser.ShowDialog()
if($dialogResult -eq [System.Windows.Forms.DialogResult]::Cancel) {
return ;
}
$CertificatePath = $SaveChooser.Filename ;
$certificatePassword = Read-host "Please provide a password for the exported certificate." -AsSecureString
$certParameters = #{} ;
$certParameters.CertStoreLocation = $CertStoreLocation;
$certParameters.Subject = $Subject;
$certParameters.KeySpec = "KeyExchange";
$certParameters.HashAlgorithm = $HashAlgorithm;
$certParameters.CertStoreLocation = $CertStoreLocation;
if ($NotAfter -ne $null) {
$certParameters.NotAfter = $NotAfter;
}
$global:CertificateInfo.Certificate = New-SelfSignedCertificate #certParameters ;
$certificateLocation = $CertStoreLocation + "\" + $global:CertificateInfo.Certificate.Thumbprint ;
Export-PfxCertificate -Cert $certificateLocation -FilePath "$($CertificatePath)" -Password $certificatePassword
}
This is how I invoke the code above:
Connect-Azure -TenantId "your tenant ID here"
Select-Certificate -ThumbPrint "your thumbprint here"
Add-AzureADApplicationKeyCredential -DisplayName "your-displayname-here"
Shown Below are entries in my application manifest the 1st I added manually and the 2nd I added using New-AzureADApplicationKeyCredential cmdlet. They are the same certificate
{
"customKeyIdentifier": "7IzWH13za4loQI6RvMtQ4uewh1g=",
"endDate": "2019-12-15T16:49:37Z",
"keyId": "fd7be8fc-e44f-4d46-a0e4-fc4ef71b0833",
"startDate": "2017-12-18T19:12:15Z",
"type": "AsymmetricX509Cert",
"usage": "Verify",
"value": null
},
{
"customKeyIdentifier": "N0l6V0gxM3phNGxvUUk2UnZNdFE0dWV3aDFnPQ==",
"endDate": "2019-12-18T19:22:10Z",
"keyId": "04b0e6a9-bac5-4d3f-be5e-57ddc2976886",
"startDate": "2017-12-19T15:47:15.9136239Z",
"type": "AsymmetricX509Cert",
"usage": "Verify",
"value": null
},
Finally a screen shot from the Keys menu in the app registration
As you can see the only way to preserve the thumbprint seems to be to NOT use the New-AzureADApplicationKeyCredential cmdlet
I found the answer. I used ILSPY to examine the source code and the cmdlet takes the string and converts it into a byte[] using
keyCredential.CustomKeyIdentifier = Encoding.ASCII.GetBytes(this.CustomKeyIdentifier);
and while the resulting certificate works the keyCertificateIdentifier is obviously encoded and displayed incorrectly. BTW I did 3 days of searching and used code from various places on the web and everyone I copied from still had it wrong.
Simple solution was to call cmdlet Set-AzureADApplication rather than cmdlet New-AzureADApplicationKeyCredential. This requires me to create a Microsoft.Open.AzureAD.Model.KeyCredential in powershell. Feel free to use the code below
$global:CertificateInfo = #{} ;
function Connect-Azure {
[CmdletBinding()]
param
(
[parameter(Mandatory=$true)]
[string] $TenantId
)
Write-Host "Connect-Azure - Enter - $($MyInvocation.MyCommand.Name)"
Write-Host "Get-ApplicatonKeyCredentials - Parameters"
Write-Host " TenantId - $($TenantId)"
$ErrorActionPreference = 'Stop';
Connect-AzureAD -TenantId $TenantId
Write-Host "Connect-Azure - Exit - $($MyInvocation.MyCommand.Name)"
}
function Add-AzureADApplicationKeyCredential {
[CmdletBinding()]
param
(
[parameter(Mandatory=$true)]
[string] $DisplayName,
[parameter(Mandatory=$false)]
[Switch] $Force
)
Write-Host "Add-AzureADApplicationKeyCredential - Enter - $($MyInvocation.MyCommand.Name)"
Write-Host "Add-AzureADApplicationKeyCredential - Parameters"
Write-Host " DisplayName - $($DisplayName)"
$ErrorActionPreference = 'Stop';
if ($global:CertificateInfo.Certificate -eq $null)
{
throw "No certificate has been selected or created yet."
}
$filter = "DisplayName eq '" + $($DisplayName) + "'" ;
$global:CertificateInfo.Application = Get-AzureADApplication -filter $filter
$certificateThumbprint = [System.Convert]::ToBase64String($global:CertificateInfo.Certificate.GetCertHash()) ;
foreach($keyCredential in $global:CertificateInfo.Application.KeyCredentials)
{
[String]$keyCredentialThumbPrint = [System.Convert]::ToBase64String($keyCredential.CustomkeyIdentifier) ;
if([String]::Equals($keyCredentialThumbPrint,$certificateThumbprint,[StringComparison]::CurrentCultureIgnoreCase)) {
throw "This certificate already exists within the keyCredentials collection with KeyId" + "'" + $keyCredential.KeyId + "'" ;
}
}
$keycredential = New-Object Microsoft.Open.AzureAD.Model.KeyCredential
$keycredential.CustomKeyIdentifier = $global:CertificateInfo.Certificate.GetCertHash() ;
$keycredential.Value = $global:CertificateInfo.Certificate.GetRawCertData() ;
$keycredential.EndDate = $global:CertificateInfo.Certificate.NotAfter ;
$keycredential.StartDate = $global:CertificateInfo.Certificate.NotBefore ;
$keycredential.Type = "AsymmetricX509Cert"
$keycredential.Usage = "Verify" ;
$keycredential.KeyId = [Guid]::NewGuid().ToString() ;
$global:CertificateInfo.Application.KeyCredentials.Add($keycredential) ;
Set-AzureADApplication -ObjectID $global:CertificateInfo.Application.ObjectId -KeyCredentials $global:CertificateInfo.Application.KeyCredentials
Write-Host "Add-AzureADApplicationKeyCredential - Exit"
}
function Select-Certificate {
[CmdletBinding()]
param
(
[parameter(Mandatory=$false)]
[string] $CertStoreLocation = "Cert:\LocalMachine\My",
[parameter(Mandatory=$true)]
[string] $ThumbPrint
)
Write-Host "Create-SelfSignedCertificate - Enter - $($MyInvocation.MyCommand.Name)"
Write-Host "Get-ApplicatonKeyCredentials - Parameters" $ErrorActionPreference = 'Stop';
$certificateLocation = $CertStoreLocation + "\" + $ThumbPrint ;
$global:CertificateInfo.Certificate = (Get-ChildItem –Path "$($certificateLocation)")
}
function Create-SelfSignedCertificate {
[CmdletBinding()]
param
(
[parameter(Mandatory=$false)]
[string] $Subject,
[parameter(Mandatory=$false)]
[string] $HashAlgorithm = "SHA256",
[parameter(Mandatory=$false)]
[string] $CertStoreLocation = "Cert:\LocalMachine\My",
[parameter(Mandatory=$true)]
$NotAfter
)
## see https://blogs.technet.microsoft.com/scotts-it-blog/2014/12/30/working-with-certificates-in-powershell/
Write-Host "Create-SelfSignedCertificate - Enter - $($MyInvocation.MyCommand.Name)"
Write-Host "Get-ApplicatonKeyCredentials - Parameters"
if([string]::IsNullOrEmpty($Subject)) {
$currentDate = (Get-Date) ;
$Subject = [String]::Format("SelfSigned{0:yyyymmddHHMMss}",$currentDate) ;
}
Write-Host " Subject - $($Subject)"
Write-Host " HashAlgorithm - $($HashAlgorithm)"
Write-Host " CertStoreLocation - $($CertStoreLocation)"
Write-Host " NotAfter - $($NotAfter)"
$ErrorActionPreference = 'Stop';
Write-Host "Create-SelfSignedCertificate - Exit - $($MyInvocation.MyCommand.Name)"
$SaveChooser = New-Object -Typename System.Windows.Forms.SaveFileDialog
$SaveChooser.CreatePrompt = $false ;
$SaveChooser.Title = "Save certficate" ;
$SaveChooser.DefaultExt = "pfx" ;
$dialogResult = $SaveChooser.ShowDialog()
if($dialogResult -eq [System.Windows.Forms.DialogResult]::Cancel) {
return ;
}
$CertificatePath = $SaveChooser.Filename ;
$certificatePassword = Read-host "Please provide a password for the exported certificate." -AsSecureString
$certParameters = #{} ;
$certParameters.CertStoreLocation = $CertStoreLocation;
$certParameters.Subject = $Subject;
$certParameters.KeySpec = "KeyExchange";
$certParameters.HashAlgorithm = $HashAlgorithm;
$certParameters.CertStoreLocation = $CertStoreLocation;
if ($NotAfter -ne $null) {
$certParameters.NotAfter = $NotAfter;
}
$global:CertificateInfo.Certificate = New-SelfSignedCertificate #certParameters ;
$certificateLocation = $CertStoreLocation + "\" + $global:CertificateInfo.Certificate.Thumbprint ;
Export-PfxCertificate -Cert $certificateLocation -FilePath "$($CertificatePath)" -Password $certificatePassword
}
this results in a manifest that looks like
"keyCredentials": [
{
"customKeyIdentifier": "KjS6U6xucxo5kuI1YAwykzrmBKE=",
"endDate": "2019-12-19T19:34:29Z",
"keyId": "de9bd300-ecdc-43d0-a5a6-e946cce10019",
"startDate": "2017-12-19T19:24:50Z",
"type": "AsymmetricX509Cert",
"usage": "Verify",
"value": null
}
],
and the display of the keys in the Azure portal looks like this
the Base64 string in the manifest 'KjS6U6xucxo5kuI1YAwykzrmBKE=' now shows up correctly as the hex representation of the certificate thumbprint '2A34BA53AC6E731A3992E235600C32933AE604A1'.
So in closing:
The duplicate check works correctly.
The Azure portal correctly shows the thumbprint of the certificate being used.
The process is automated to reduce the possibility of cut/paste errors.
Does anyone know how Azure is modifying the customKeyIdentifier so
that I can get my duplicate check to work?
First, you're right. Azure allows you upload one certificates for many times. But each uploading action will get one unique KeyId for the certificate.
I understand what you want to achieve. But My scripts is for find whether the certificate is already uploaded. If you want to duplicate check on the uploaded certificates, I think it be better go to Azure portal and find the same certificate quickly, rather than using Powershell. Here is my scripts:
$certs = Get-AzureADApplicationKeyCredential -ObjectId 25f83866-561f-4cf2-b7a6-d623d55864df
$base64Thumbprint = [System.Convert]::ToBase64String($cer.GetCertHash()) # $cer is your local certificate
Foreach ($certificate in $certs)
{
$customkeyIdentifier = $certificate.CustomKeyIdentifier
$UploadedThumbprint = [System.Convert]::ToBase64String($customkeyidentifier)
If($UploadedThumbprint -eq $base64Thumbprint)
{
Write-Host "This certificate is same as yours and its KeyId is" : $certificate.keyId -ForegroundColor Red
}
else
{
Write-Host "This certificate is different from your cert and its KeyId is" : $certificate.keyId -ForegroundColor Cyan
}
}
Here is my result:
Hope this helps!

Powershell resultset not being picked up in C#

When button1 is clicked, the below code is executed which will run a PowerShell script to get the current SQL Server Instances. However when this is run, the result set (results variable) has a count of 0 rows from the PowerShell output. When I run the same code in native PowerShell it displays 3 rows with the instance names.
Can anyone advise if I am missing something?
private void button1_Click(object sender, EventArgs e)
{
//If the logPath exists, delete the file
string logPath = "Output.Log";
if (File.Exists(logPath))
{
File.Delete(logPath);
}
string[] Servers = richTextBox1.Text.Split('\n');
//Pass each server name from the listview to the 'Server' variable
foreach (string Server in Servers)
{
//PowerShell Script
string PSScript = #"
param([Parameter(Mandatory = $true, ValueFromPipeline = $true)][string] $server)
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force;
Import-Module SQLServer;
Try
{
Set-Location SQLServer:\\SQL\\$server -ErrorAction Stop;
Get-ChildItem | Select-Object -ExpandProperty Name;
}
Catch
{
echo 'No SQL Server Instances';
}
";
//Create PowerShell Instance
PowerShell psInstance = PowerShell.Create();
//Add PowerShell Script
psInstance.AddScript(PSScript);
//Pass the Server variable in to the $server parameter within the PS script
psInstance.AddParameter("server", Server);
//Execute Script
Collection<PSObject> results = new Collection<PSObject>();
try
{
results = psInstance.Invoke();
}
catch (Exception ex)
{
results.Add(new PSObject((Object)ex.Message));
}
//Loop through each of the results in the PowerShell window
foreach (PSObject result in results)
{
File.AppendAllText(logPath, result + Environment.NewLine);
// listBox1.Items.Add(result);
}
psInstance.Dispose();
}
}
To get an possible PowerShell error I would try sth. like this:
private void button1_Click(object sender, EventArgs e)
{
//If the logPath exists, delete the file
string logPath = "Output.Log";
if (File.Exists(logPath)) {
File.Delete(logPath);
}
string[] Servers = richTextBox1.Text.Split('\n');
//Pass each server name from the listview to the 'Server' variable
foreach (string Server in Servers) {
//PowerShell Script
string PSScript = #"
param([Parameter(Mandatory = $true, ValueFromPipeline = $true)][string] $server)
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force;
Import-Module SQLServer;
Try
{
Set-Location SQLServer:\\SQL\\$server -ErrorAction Stop;
Get-ChildItem | Select-Object -ExpandProperty Name;
}
Catch
{
echo 'No SQL Server Instances';
}
";
using (PowerShell psInstance = PowerShell.Create()) {
psInstance.AddScript(PSScript);
psInstance.AddParameter("server", Server);
Collection<PSObject> results = psInstance.Invoke();
if (psInstance.Streams.Error.Count > 0) {
foreach (var errorRecord in psInstance.Streams.Error) {
MessageBox.Show(errorRecord.ToString());
}
}
foreach (PSObject result in results) {
File.AppendAllText(logPath, result + Environment.NewLine);
// listBox1.Items.Add(result);
}
}
}
}
The reason it isn't working is that psInstance.AddParameter only adds parameters to commands, it doesn't work with a script. You'll need to find another way of getting the $server parameter into the script. Try these two powershell examples to see what I mean. The first will output all processes (ignores the AddParameter) while the second only shows svchost processes.
1)
$ps = [system.management.automation.powershell]::create()
$ps.AddScript("get-process")
$ps.AddParameter("name","svchost")
$ps.invoke()
2)
$ps = [system.management.automation.powershell]::create()
$ps.AddCommand("get-process")
$ps.AddParameter("name","svchost")
$ps.invoke()
I managed to get round this issue by using the Win32_service instead of SQLPS.
Param([Parameter(Mandatory = $true, ValueFromPipeline = $true)][string] $server)
$localInstances = #()
[array]$captions = GWMI Win32_Service -ComputerName $server | ?{$_.Name -match 'mssql *' -and $_.PathName -match 'sqlservr.exe'} | %{$_.Caption}
ForEach($caption in $captions)
{
if ($caption -eq 'MSSQLSERVER')
{
$localInstances += 'MSSQLSERVER'
}
else
{
$temp = $caption | %{$_.split(' ')[-1]} | %{$_.trimStart('(')} | %{$_.trimEnd(')')}
$localInstances += ""$server\$temp""
}
}
$localInstances;

PowerShell error : Cannot Invoke this function because the current host does not implement it

function LogMe() {
Param(
[parameter(Mandatory = $true, ValueFromPipeline = $true)]$logEntry,
[switch]$display,
[switch]$error,
[switch]$warning,
[switch]$progress
)
if ($error) {
$logEntry = "[ERROR] $logEntry"
Write-Host "$logEntry" -ForegroundColor Red
} elseif ($warning) {
Write-Warning "$logEntry"
$logEntry = "[WARNING] $logEntry"
} elseif ($progress) {
Write-Host "$logEntry" -ForegroundColor Green
} elseif ($display) {
Write-Host "$logEntry"
}
#$logEntry = ((Get-Date -uformat "%D %T") + " - " + $logEntry)
$logEntry | -force Out-File $logFile -Append
}
The above code spits out an error described in the title. I tried -force any other suggestions?
It's not C# per se but the error I'm getting is coming from C# when I run using pipeline.invoke.

Categories