Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d22ff2b
Implement Revoke-SCEPmanCertificate
cheinzler-gk May 19, 2026
cbc2c0e
Improve error handling by parsing internal error codes
cheinzler-gk May 22, 2026
b9e6f6e
Implement Find-SCEPmanCertificate
cheinzler-gk May 22, 2026
23b6977
Add tests
cheinzler-gk May 22, 2026
d4593ac
Add manage API to readme
cheinzler-gk May 22, 2026
ea9dcbf
Remove unnecessary output
cheinzler-gk May 22, 2026
59353db
Remove unnecessary output
cheinzler-gk May 22, 2026
2a51239
Don't force import in tests
cheinzler-gk May 22, 2026
a4b6f05
Merge branch 'feat/RevocationAPI' of https://github.com/scepman/scepm…
cheinzler-gk May 22, 2026
81ed063
fix: null-check HTTP response in find certificate error handling
Copilot May 22, 2026
760244e
Remove tertiary operator for PS5 compatability
cheinzler-gk May 24, 2026
85605c0
Add simpler error handling for PS5
cheinzler-gk May 24, 2026
1289b9a
Merge branch 'feat/RevocationAPI' of https://github.com/scepman/scepm…
cheinzler-gk May 24, 2026
1f45dff
Remove possible return value from result
cheinzler-gk May 27, 2026
c93a1ba
Process Url and header in Begin scope
cheinzler-gk May 27, 2026
f96ecbd
Remove initial error logging
cheinzler-gk May 27, 2026
6915b9a
Remove default parameter values from query
cheinzler-gk May 27, 2026
5eb1982
Context is already used by SCEPman. No need to duplicate it.
cheinzler-gk May 27, 2026
80efc0f
Prepare for direct token auth for SCEPman SaaS
cheinzler-gk May 27, 2026
f487f92
Simplify error handling
cheinzler-gk May 27, 2026
730d5b2
Remove obsolete test
cheinzler-gk May 27, 2026
b461242
Adjust synopsis
cheinzler-gk May 27, 2026
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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,31 @@ Please note that this requires additional SCEPman configuration regarding the st
- AppConfig:StaticValidation:AllowRenewals : true
- AppConfig:StaticValidation:ReenrollmentAllowedCertificateTypes: Static,IntuneUser (Depending on the types intended for renewal)

## Search certificates
Use `Find-SCEPmanCertificate` to query certificates via the SCEPman management API.

```powershell
Find-SCEPmanCertificate -Url 'https://scepman.contoso.com' -SearchText 'alice@contoso.com' -PageSize 50 -CertValidity Any -CertType Any
```

When paginating, pass the continuation token returned by the previous response:

```powershell
Find-SCEPmanCertificate -Url 'https://scepman.contoso.com' -SearchText 'alice' -ContinuationToken '<continuation-token>'
```

## Revoke certificates
Use `Revoke-SCEPmanCertificate` to revoke one or more certificates in SCEPman Enterprise.

Revoke a single certificate with an explicit revoker identity:

```powershell
Revoke-SCEPmanCertificate -Url 'https://scepman.contoso.com' -SerialNumber '1A2B3C4D' -RevocationReason KeyCompromise -Revoker 'admin@contoso.com'
```

Revoke multiple certificates at once:

```powershell
Revoke-SCEPmanCertificate -Url 'https://scepman.contoso.com' -SerialNumber '1A2B3C4D','5E6F7A8B' -RevocationReason Superseded
```

29 changes: 29 additions & 0 deletions SCEPmanClient/Private/x509/constants.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,33 @@ enum ValidityPeriod {
Weeks
Months
Years
}

enum RevocationReason {
# Good = -1
Unspecified = 0
KeyCompromise = 1
CACompromise = 2
AffiliationChanged = 3
Superseded = 4
CessationOfOperation = 5
CertificateHold = 6
RemoveFromCrl = 8
PrivilegeWithdrawn = 9
AACompromise = 10
}

enum CertValidityType {
Valid
Revoked
Expired
Any
}

enum CertType {
Static
DC
User
Device
Any
}
198 changes: 198 additions & 0 deletions SCEPmanClient/Public/Find-SCEPmanCertificate.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<#
.SYNOPSIS
Search certificates issued by SCEPman.

.DESCRIPTION
This function searches certificates via the SCEPman management search API.

.PARAMETER Url
The URL of the SCEPman App Service.

.PARAMETER SearchText
Search text used by the SCEPman API (for example an email, subject, or serial fragment).

.PARAMETER PageSize
Number of results to return per request.

.PARAMETER CertValidity
Certificate validity filter value expected by your SCEPman API (for example 'Any' or a numeric enum value).

.PARAMETER CertType
Certificate type filter value expected by your SCEPman API (for example 'Any' or a numeric enum value).

.PARAMETER ContinuationToken
Continuation token from a previous search response.

.PARAMETER ResourceUrl
The resource URL of the SCEPman service. If not provided, the function will try to find the Enterprise Application for the URL.

.PARAMETER IgnoreExistingSession
Ignore existing Azure session.

.PARAMETER DeviceCode
Use device code authentication.

.PARAMETER Identity
Use the managed identity for authentication.

.PARAMETER ClientId
The client ID for service principal authentication.

.PARAMETER TenantId
The tenant ID for service principal authentication.

.PARAMETER ClientSecret
The client secret for service principal authentication.

.PARAMETER AccessToken
An access token for authentication. If not provided, the function will authenticate using Azure PowerShell

.EXAMPLE
Find-SCEPmanCertificate -Url "https://scepman.contoso.com" -SearchText "alice@contoso.com" -PageSize 50 -CertValidity Any -CertType Any

.EXAMPLE
Find-SCEPmanCertificate -Url "https://scepman.contoso.com" -SearchText "alice" -ContinuationToken "next-page-token"
#>

Function Find-SCEPmanCertificate {
[CmdletBinding(DefaultParameterSetName='AzAuth')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingUsernameAndPasswordParams", "", Justification="Service principal authentication requires username and password.")]
Param(
[Parameter(Mandatory, Position=0)]
[Alias('AppServiceUrl')]
[String]$Url,

[String]$SearchText,

[ValidateRange(1, 500)]
[Int]$PageSize = 50,

[CertValidityType]$CertValidity,

[CertType]$CertType,
Comment thread
cheinzler-gk marked this conversation as resolved.

[String]$ContinuationToken,

[Parameter(ParameterSetName='AzAuth')]
[String]$ResourceUrl,

[Parameter(ParameterSetName='AzAuth')]
[Switch]$IgnoreExistingSession,
[Parameter(ParameterSetName='AzAuth')]
[Switch]$DeviceCode,
[Parameter(ParameterSetName='AzAuth')]
[Switch]$Identity,
[Parameter(ParameterSetName='AzAuth')]
[String]$ClientId,
[Parameter(ParameterSetName='AzAuth')]
[String]$TenantId,
[Parameter(ParameterSetName='AzAuth')]
[String]$ClientSecret,

[Parameter(ParameterSetName='DirectTokenAuth')]
[String]$AccessToken
)

Begin {
$ErrorActionPreference = 'Stop'

If($PSCmdlet.ParameterSetName -eq 'DirectTokenAuth') {
Write-Verbose "$($MyInvocation.MyCommand): Using direct token authentication"

If (-not $AccessToken) {
throw "$($MyInvocation.MyCommand): AccessToken is required for direct token authentication"
}
} Else {
Set-AzConfig -Scope Process -LoginExperienceV2 Off -DisplaySurveyMessage $false | Out-Null

$Connect_Params = @{}

If ($PSBoundParameters.ContainsKey('IgnoreExistingSession')) { $Connect_Params['IgnoreExistingSession'] = $true }
If ($PSBoundParameters.ContainsKey('DeviceCode')) { $Connect_Params['DeviceCode'] = $true }
If ($PSBoundParameters.ContainsKey('Identity')) { $Connect_Params['Identity'] = $true }
If ($PSBoundParameters.ContainsKey('ClientId')) { $Connect_Params['ClientId'] = $ClientId }
If ($PSBoundParameters.ContainsKey('TenantId')) { $Connect_Params['TenantId'] = $TenantId }
If ($PSBoundParameters.ContainsKey('ClientSecret')) { $Connect_Params['ClientSecret'] = $ClientSecret }

Connect-SCEPmanAzAccount @Connect_Params

If (-not $PSBoundParameters.ContainsKey('ResourceUrl')) {
Write-Verbose "$($MyInvocation.MyCommand): No resource URL provided. Trying to find Enterprise Application for URL: $Url"
$ResourceUrl = Get-SCEPmanResourceUrl -AppServiceUrl $Url
}

$AccessToken = Get-SCEPmanAccessToken -ResourceUrl $ResourceUrl
}
}

Process {
$BaseUrl = $Url.TrimEnd('/')

$Headers = @{
'Authorization' = "Bearer $AccessToken"
}

$Query = [ordered]@{
SearchText = $SearchText
PageSize = $PageSize
CertValidity = $CertValidity
CertType = $CertType
ContinuationToken = $ContinuationToken
}

$QueryString = ($Query.GetEnumerator() |
Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.Value) } |
ForEach-Object {
'{0}={1}' -f [uri]::EscapeDataString($_.Key), [uri]::EscapeDataString([string]$_.Value)
}) -join '&'
Comment thread
cheinzler-gk marked this conversation as resolved.

$RequestUrl = "$BaseUrl/api/manage/search"
If (-not [string]::IsNullOrWhiteSpace($QueryString)) {
$RequestUrl = "{0}?{1}" -f $RequestUrl, $QueryString
}

Write-Verbose "$($MyInvocation.MyCommand): Sending search request to $RequestUrl"

try {
$Response = Invoke-RestMethod -Uri $RequestUrl -Method Get -Headers $Headers
Write-Verbose "$($MyInvocation.MyCommand): Search request successful. Found $($Response.items.count) certificate$(if($Response.items.count -ne 1) { 's' })"

Return $Response

} catch {
$StatusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { $null }
$RawErrorBody = $_.ErrorDetails.Message

Comment thread
cheinzler-gk marked this conversation as resolved.
$ApiErrorCode = $null
$ApiErrorMessage = $null

if ($RawErrorBody) {
try {
$ParsedError = $RawErrorBody | ConvertFrom-Json
$ApiErrorCode = $ParsedError.ErrorCode
$ApiErrorMessage = $ParsedError.ErrorMessage
}
catch {
$ApiErrorMessage = $RawErrorBody
}
}

Write-Verbose "$($MyInvocation.MyCommand): Failed to search certificates. Status code: $StatusCode. ApiErrorCode: $ApiErrorCode. ApiErrorMessage: $ApiErrorMessage"

switch ($ApiErrorCode) {
4711 { throw "$($MyInvocation.MyCommand): SCEPman Enterprise is required for the manage search API." }
default {
switch ($StatusCode) {
400 { throw "$($MyInvocation.MyCommand): Bad request. Check the request parameters for errors. $ApiErrorMessage" }
401 { throw "$($MyInvocation.MyCommand): Unauthorized. Authentication failed." }
403 { throw "$($MyInvocation.MyCommand): Forbidden. Access denied or license revoked." }
404 { throw "$($MyInvocation.MyCommand): Endpoint not found. Verify the URL and that the manage API endpoint exists." }
409 { throw "$($MyInvocation.MyCommand): Conflict. $(if ($ApiErrorMessage) { $ApiErrorMessage } else { 'Request could not be completed.' })" }
500 { throw "$($MyInvocation.MyCommand): Server error while searching certificates. $ApiErrorMessage" }
default { throw $_ }
}
}
}
}
}
}
Loading
Loading