Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions AzureKeyVault/AzureClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
using Keyfactor.Orchestrators.Common.Enums;
using Keyfactor.Orchestrators.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.VisualBasic;

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

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

Unused using Microsoft.VisualBasic; appears to have been added but nothing in this file references it. Please remove the unused import to avoid unnecessary dependencies and compiler warnings.

Suggested change
using Microsoft.VisualBasic;

Copilot uses AI. Check for mistakes.
using Newtonsoft.Json;

namespace Keyfactor.Extensions.Orchestrator.AzureKeyVault
{
Expand All @@ -38,8 +40,6 @@ private Uri AzureCloudEndpoint
case "china":
logger.LogTrace(AzureAuthorityHosts.AzureChina.ToString());
return AzureAuthorityHosts.AzureChina;
//case "germany":
// return AzureAuthorityHosts.AzureGermany; // germany is no longer a valid azure authority host as of 2021
case "government":
logger.LogTrace(AzureAuthorityHosts.AzureGovernment.ToString());
return AzureAuthorityHosts.AzureGovernment;
Expand Down Expand Up @@ -304,15 +304,31 @@ public virtual async Task<IEnumerable<CurrentInventoryItem>> GetCertificatesAsyn
{
var cert = await CertClient.GetCertificateAsync(certificate.Name);
logger.LogTrace($"got certificate details");
logger.LogTrace($"cert properties: {JsonConvert.SerializeObject(cert.Value?.Properties)}");
var itemEntryParams = new Dictionary<string, object>();

if (cert.Value?.Properties?.Tags != null && cert.Value.Properties.Tags.Count > 0) { // set tags entry parameter to value
itemEntryParams.Add(EntryParameters.TAGS, JsonConvert.SerializeObject(cert.Value.Properties.Tags));
}

if (cert.Value.Policy != null) // set nonexportable entry parameter to value
{
var exportable = cert.Value.Policy?.Exportable;
itemEntryParams.Add(EntryParameters.NON_EXPORTABLE, !exportable);

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

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

cert.Value.Policy?.Exportable is nullable; using !exportable can yield null, which means NonExportable may be returned as a null value even though the store type defines it as a Bool. Consider normalizing this to a concrete boolean (or omitting the parameter when the exportable state is unknown) so inventory consistently returns the expected type.

Suggested change
itemEntryParams.Add(EntryParameters.NON_EXPORTABLE, !exportable);
if (exportable.HasValue)
{
itemEntryParams.Add(EntryParameters.NON_EXPORTABLE, !exportable.Value);
}

Copilot uses AI. Check for mistakes.
}

itemEntryParams.Add(EntryParameters.PRESERVE_TAGS, null); // we can never know this; it's only evaluated on enrollment; set to null

logger.LogTrace($"evaluated entry parameters to be returned: {JsonConvert.SerializeObject(itemEntryParams)}");

inventoryItems.Add(new CurrentInventoryItem()
{
Alias = cert.Value.Name,
PrivateKeyEntry = true,
ItemStatus = OrchestratorInventoryItemStatus.Unknown,
UseChainLevel = true,
Certificates = new List<string>() { Convert.ToBase64String(cert.Value.Cer) },
Parameters = cert.Value.Properties.Tags as Dictionary<string, object>
Parameters = itemEntryParams
});
}
catch (Exception ex)
Expand Down
2 changes: 1 addition & 1 deletion AzureKeyVault/Jobs/Management.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public JobResult ProcessJob(ManagementJobConfiguration config)
logger.LogTrace("parsing entry parameters.. ");

tagsJSON = config.JobProperties[EntryParameters.TAGS] as string ?? string.Empty;
preserveTags = config.JobProperties[EntryParameters.PRESERVE_TAGS] as bool? ?? false;
preserveTags = config.JobProperties[EntryParameters.PRESERVE_TAGS] as bool? ?? true;

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

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

PreserveExistingTags is documented/declared with a default of False (see integration-manifest and store-type scripts), but the runtime default here is true when the entry parameter is missing/null. This changes behavior (tags will be preserved even if the user didn't opt in). Consider defaulting to false and only enabling preservation when the entry parameter is explicitly set to true.

Suggested change
preserveTags = config.JobProperties[EntryParameters.PRESERVE_TAGS] as bool? ?? true;
preserveTags = config.JobProperties[EntryParameters.PRESERVE_TAGS] as bool? ?? false;

Copilot uses AI. Check for mistakes.
nonExportable = config.JobProperties[EntryParameters.NON_EXPORTABLE] as bool? ?? false;

switch (config.OperationType)
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@

- 3.2.2
- Updated screenshots in README
- Returning entry parameters along with inventory
- 3.2.1
- Documentation updates and improvements
- Updated NuGet packages
Expand Down
Binary file modified docsource/images/AKV-entry-parameters-store-type-dialog.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
185 changes: 185 additions & 0 deletions scripts/store_types/bash/curl_create_store_types.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#!/usr/bin/env bash

# Creates all 1 store types via the Keyfactor Command REST API using curl.
#
# Authentication (first matching method is used):
# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN
# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET
# + KEYFACTOR_AUTH_TOKEN_URL
# Basic auth (AD): KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN
#
# Always required:
# KEYFACTOR_HOSTNAME Command hostname (e.g. my-command.example.com)
#
# Auto-generated by doctool generate-store-type-scripts — do not edit by hand.

if [ -z "${KEYFACTOR_HOSTNAME}" ]; then
echo "ERROR: KEYFACTOR_HOSTNAME is required"
exit 1
fi

BASE_URL="https://${KEYFACTOR_HOSTNAME}/keyfactorapi"

# ---------------------------------------------------------------------------
# Resolve auth
# ---------------------------------------------------------------------------
if [ -n "${KEYFACTOR_AUTH_ACCESS_TOKEN}" ]; then
BEARER_TOKEN="${KEYFACTOR_AUTH_ACCESS_TOKEN}"
elif [ -n "${KEYFACTOR_AUTH_CLIENT_ID}" ] && [ -n "${KEYFACTOR_AUTH_CLIENT_SECRET}" ] && [ -n "${KEYFACTOR_AUTH_TOKEN_URL}" ]; then
echo "Fetching OAuth token..."
BEARER_TOKEN=$(curl -s -X POST "${KEYFACTOR_AUTH_TOKEN_URL}" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=${KEYFACTOR_AUTH_CLIENT_ID}" \
--data-urlencode "client_secret=${KEYFACTOR_AUTH_CLIENT_SECRET}" | jq -r '.access_token')
if [ -z "${BEARER_TOKEN}" ] || [ "${BEARER_TOKEN}" = "null" ]; then
echo "ERROR: Failed to fetch OAuth token from ${KEYFACTOR_AUTH_TOKEN_URL}"
exit 1
Comment on lines +28 to +37

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

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

The OAuth token flow pipes the token response through jq, but the script never checks that jq is installed. Add a dependency check (similar to the kfutil scripts) or avoid jq so the script fails with a clear error instead of command not found.

Copilot uses AI. Check for mistakes.
fi
elif [ -n "${KEYFACTOR_USERNAME}" ] && [ -n "${KEYFACTOR_PASSWORD}" ] && [ -n "${KEYFACTOR_DOMAIN}" ]; then
BEARER_TOKEN=""
else
echo "ERROR: Authentication required. Set one of:"
echo " KEYFACTOR_AUTH_ACCESS_TOKEN"
echo " KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET + KEYFACTOR_AUTH_TOKEN_URL"
echo " KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN"
exit 1
fi

if [ -n "${BEARER_TOKEN}" ]; then
CURL_AUTH=("-H" "Authorization: Bearer ${BEARER_TOKEN}")
else
CURL_AUTH=("-u" "${KEYFACTOR_USERNAME}@${KEYFACTOR_DOMAIN}:${KEYFACTOR_PASSWORD}")
fi

create_store_type() {
local name="$1"
local body="$2"
echo "Creating ${name} store type..."
response=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "${BASE_URL}/certificatestoretypes" \
-H "Content-Type: application/json" \
-H "x-keyfactor-requested-with: APIClient" \
"${CURL_AUTH[@]}" \
-d "${body}")
if [ "$response" = "200" ] || [ "$response" = "201" ]; then
echo " OK (HTTP ${response})"
else
echo " FAILED (HTTP ${response})"
fi
}

# ---------------------------------------------------------------------------
# AKV — The GUID of the tenant ID of the Azure Keyvault instance; for example, '12345678-1234-1234-1234-123456789abc'.
# ---------------------------------------------------------------------------
create_store_type "AKV" '{
"BlueprintAllowed": false,
"Capability": "AKV",
"CustomAliasAllowed": "Optional",
"EntryParameters": [
{
"Name": "CertificateTags",
"DisplayName": "Certificate Tags",
"Description": "If desired, tags can be applied to the KeyVault entries. Provide them as a JSON string of key-value pairs ie: '{'tag-name': 'tag-content', 'other-tag-name': 'other-tag-content'}'",
"Type": "string",
"DefaultValue": "",
"RequiredWhen": {
"HasPrivateKey": false,
"OnAdd": false,
"OnRemove": false,
"OnReenrollment": false
}
},
{
"Name": "PreserveExistingTags",
"DisplayName": "Preserve Existing Tags",
"Description": "If true, this will perform a union of any tags provided with enrollment with the tags on the existing cert with the same alias and apply the result to the new certificate.",
"Type": "Bool",
"DefaultValue": "False",
"RequiredWhen": {
"HasPrivateKey": false,
"OnAdd": false,
"OnRemove": false,
"OnReenrollment": false
}
},
{
"Name": "NonExportable",
"DisplayName": "Non Exportable Private Key",
"Description": "If true, this will mark the certificate as having a non-exportable private key when importing into Azure KeyVault",
"Type": "Bool",
"DefaultValue": "False",
"RequiredWhen": {
"HasPrivateKey": false,
"OnAdd": false,
"OnRemove": false,
"OnReenrollment": false
}
}
],
"JobProperties": [],
"LocalStore": false,
"Name": "Azure Keyvault",
"PasswordOptions": {
"EntrySupported": false,
"StoreRequired": false,
"Style": "Default"
},
"PowerShell": false,
"PrivateKeyAllowed": "Optional",
"Properties": [
{
"Name": "TenantId",
"DisplayName": "Tenant Id",
"Type": "String",
"DependsOn": "",
"Required": false
},
{
"Name": "SkuType",
"DisplayName": "SKU Type",
"Type": "MultipleChoice",
"DependsOn": "",
"DefaultValue": "standard,premium",
"Required": false
},
{
"Name": "VaultRegion",
"DisplayName": "Vault Region",
"Type": "MultipleChoice",
"DependsOn": "",
"DefaultValue": "eastus,eastus2,westus2,westus3,westus",
"Required": false
},
{
"Name": "AzureCloud",
"DisplayName": "Azure Cloud",
"Type": "MultipleChoice",
"DependsOn": "",
"DefaultValue": "public,china,government",
"Required": false
},
{
"Name": "PrivateEndpoint",
"DisplayName": "Private KeyVault Endpoint",
"Type": "String",
"DependsOn": "",
"Required": false
}
],
"ServerRequired": true,
"ShortName": "AKV",
"StorePathDescription": "A string formatted as '{subscription id}:{resource group name}:{vault name}'; for example, '12345678-1234-1234-1234-123456789abc:myResourceGroup:myVault'.",
"StorePathType": "",
"StorePathValue": "",
"SupportedOperations": {
"Add": true,
"Create": true,
"Discovery": true,
"Enrollment": false,
"Remove": true
}
}'


echo "Completed."
28 changes: 28 additions & 0 deletions scripts/store_types/bash/kfutil_create_store_types.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash

# Creates all 1 store types using kfutil.
# kfutil reads definitions from the Keyfactor integration catalog.
#
# Auth environment variables (first matching method is used):
# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN
# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET
# + KEYFACTOR_AUTH_TOKEN_URL
# Basic auth (AD): KEYFACTOR_HOSTNAME + KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD
# + KEYFACTOR_DOMAIN
#
# Auto-generated by doctool generate-store-type-scripts — do not edit by hand.

if ! command -v kfutil &> /dev/null; then
echo "kfutil could not be found. Please install kfutil"
echo "See https://github.com/Keyfactor/kfutil#quickstart"
exit 1
fi

if [ -z "$KEYFACTOR_HOSTNAME" ]; then
echo "KEYFACTOR_HOSTNAME not set — launching kfutil login"
kfutil login
fi

kfutil store-types create --name "AKV"

echo "Done. All store types created."
29 changes: 29 additions & 0 deletions scripts/store_types/powershell/kfutil_create_store_types.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Creates all 1 store types using kfutil.
# kfutil reads definitions from the Keyfactor integration catalog.
#
# Auth environment variables (first matching method is used):
# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN
# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET
# + KEYFACTOR_AUTH_TOKEN_URL
# Basic auth (AD): KEYFACTOR_HOSTNAME + KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD
# + KEYFACTOR_DOMAIN
#
# Auto-generated by doctool generate-store-type-scripts — do not edit by hand.

# Uncomment if kfutil is not in your PATH
# Set-Alias -Name kfutil -Value 'C:\Program Files\Keyfactor\kfutil\kfutil.exe'

if ($null -eq (Get-Command "kfutil" -ErrorAction SilentlyContinue)) {
Write-Host "kfutil could not be found. Please install kfutil"
Write-Host "See https://github.com/Keyfactor/kfutil#quickstart"
exit 1
}

if (-not $env:KEYFACTOR_HOSTNAME) {
Write-Host "KEYFACTOR_HOSTNAME not set — launching kfutil login"
& kfutil login
}

& kfutil store-types create --name "AKV"

Write-Host "Done. All store types created."
Loading
Loading