SymantecvipUserManager.psm1
|
#Requires -Version 5.1 Set-StrictMode -Version Latest # --------------------------------------------------------------------------- # SymantecvipUserManager module # Reverse engineered from SymantecUserManager.jar (com.tangynt.*). # Preserves the exact SOAP envelopes, endpoints and placeholders used by the # original Java tool so responses are byte-for-byte compatible with what the # Symantec / Broadcom VIP service expects. # --------------------------------------------------------------------------- # region: SOAP templates copied verbatim from the JAR ------------------------ # Each template uses the tokens __request_id__, __cred_id__, __cred_2_id__, # __cred_3_id__ and __cred_4_id__ exactly as the Java tool does (see # com.tangynt.shared.ApiInterface). $script:VipOperations = @{ ActivateToken = @{ Endpoint = 'https://services-auth.vip.symantec.com/mgmt/soap' Template = @' <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:vip="https://schemas.vip.symantec.com/2006/08/vipservice"> <soapenv:Header/> <soapenv:Body> <vip:ActivateToken Version="3.0" Id="__request_id__"> <vips:AuthorizerAccountId>__cred_id__</vips:AuthorizerAccountId> <vip:TokenId>__cred_2_id__</vip:TokenId> </vip:ActivateToken> </soapenv:Body></soapenv:Envelope> '@ Required = 2 } DisableToken = @{ Endpoint = 'https://services-auth.vip.symantec.com/mgmt/soap' Template = @' <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:vip="https://schemas.vip.symantec.com/2006/08/vipservice" xmlns:xd="http://www.w3.org/2000/09/xmldsig#"><soapenv:Header/><soapenv:Body><vip:DisableToken Version="3.0" Id="__request_id__"><vips:AuthorizerAccountId>__cred_id__</vips:AuthorizerAccountId><vip:TokenId>__cred_2_id__</vip:TokenId></vip:DisableToken></soapenv:Body></soapenv:Envelope> '@ Required = 2 } EnableToken = @{ Endpoint = 'https://services-auth.vip.symantec.com/mgmt/soap' Template = @' <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:vip="https://schemas.vip.symantec.com/2006/08/vipservice" xmlns:xd="http://www.w3.org/2000/09/xmldsig#"><soapenv:Header/><soapenv:Body><vip:EnableToken Version="3.0" Id="__request_id__"><vips:AuthorizerAccountId>__cred_id__</vips:AuthorizerAccountId><vip:TokenId>__cred_2_id__</vip:TokenId></vip:EnableToken></soapenv:Body></soapenv:Envelope> '@ Required = 2 } GetTokenInformation = @{ Endpoint = 'https://services-auth.vip.symantec.com/mgmt/soap' Template = @' <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:vip="https://schemas.vip.symantec.com/2006/08/vipservice" xmlns:xd="http://www.w3.org/2000/09/xmldsig#"><soapenv:Header/><soapenv:Body><vip:GetTokenInformation Version="3.0" Id="__request_id__"><vip:AuthorizerAccountId>__cred_id__</vip:AuthorizerAccountId><vip:TokenId>__cred_2_id__</vip:TokenId></vip:GetTokenInformation></soapenv:Body></soapenv:Envelope> '@ Required = 2 } UnlockToken = @{ Endpoint = 'https://services-auth.vip.symantec.com/mgmt/soap' Template = @' <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:vip="https://schemas.vip.symantec.com/2006/08/vipservice" xmlns:xd="http://www.w3.org/2000/09/xmldsig#"><soapenv:Header/><soapenv:Body><vip:UnlockToken Version="3.0" Id="__request_id__"><vips:AuthorizerAccountId>__cred_id__</vips:AuthorizerAccountId><vip:TokenId>__cred_2_id__</vip:TokenId></vip:UnlockToken></soapenv:Body></soapenv:Envelope> '@ Required = 2 } UnlockSmsToken = @{ Endpoint = 'https://services-auth.vip.symantec.com/mgmt/soap' Template = @' <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:vip="https://schemas.vip.symantec.com/2006/08/vipservice" xmlns:xd="http://www.w3.org/2000/09/xmldsig#"><soapenv:Header/><soapenv:Body><vip:UnlockToken Version="3.0" Id="__request_id__"><vip:AuthorizerAccountId>__cred_id__</vip:AuthorizerAccountId><vip:TokenId type="SMS">__cred_2_id__</vip:TokenId></vip:UnlockToken></soapenv:Body></soapenv:Envelope> '@ Required = 2 } UnlockVoiceToken = @{ Endpoint = 'https://services-auth.vip.symantec.com/mgmt/soap' Template = @' <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:vip="https://schemas.vip.symantec.com/2006/08/vipservice" xmlns:xd="http://www.w3.org/2000/09/xmldsig#"><soapenv:Header/><soapenv:Body><vip:UnlockToken Version="3.0" Id="__request_id__"><vip:AuthorizerAccountId>__cred_id__</vip:AuthorizerAccountId><vip:TokenId type="Voice">__cred_2_id__</vip:TokenId></vip:UnlockToken></soapenv:Body></soapenv:Envelope> '@ Required = 2 } CreateUser = @{ Endpoint = 'https://userservices-auth.vip.symantec.com/vipuserservices/ManagementService_1_7' Template = @' <soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'> <soapenv:Body> <vip:CreateUserRequest xmlns:vip='https://schemas.symantec.com/vip/2011/04/vipuserservices'> <vip:requestId>__request_id__</vip:requestId> <vip:onBehalfOfAccountId>__cred_id__</vip:onBehalfOfAccountId> <vip:userId>__cred_2_id__</vip:userId> </vip:CreateUserRequest> </soapenv:Body></soapenv:Envelope> '@ Required = 2 } DeleteUser = @{ Endpoint = 'https://userservices-auth.vip.symantec.com/vipuserservices/ManagementService_1_7' Template = @' <soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'> <soapenv:Body> <vip:DeleteUserRequest xmlns:vip='https://schemas.symantec.com/vip/2011/04/vipuserservices'> <vip:requestId>__request_id__</vip:requestId> <vip:onBehalfOfAccountId>__cred_id__</vip:onBehalfOfAccountId> <vip:userId>__cred_2_id__</vip:userId> </vip:DeleteUserRequest> </soapenv:Body></soapenv:Envelope> '@ Required = 2 } UpdateUser = @{ Endpoint = 'https://userservices-auth.vip.symantec.com/vipuserservices/ManagementService_1_7' Template = @' <soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'> <soapenv:Body> <vip:UpdateUserRequest xmlns:vip='https://schemas.symantec.com/vip/2011/04/vipuserservices'> <vip:requestId>__request_id__</vip:requestId> <vip:onBehalfOfAccountId>__cred_id__</vip:onBehalfOfAccountId> <vip:userId>__cred_2_id__</vip:userId> <vip:newUserId>__cred_3_id__</vip:newUserId> <vip:newUserStatus>__cred_4_id__</vip:newUserStatus> </vip:UpdateUserRequest> </soapenv:Body> </soapenv:Envelope> '@ Required = 4 } AddCredential = @{ Endpoint = 'https://userservices-auth.vip.symantec.com/vipuserservices/ManagementService_1_7' Template = @' <soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'> <soapenv:Body> <vip:AddCredentialRequest xmlns:vip='https://schemas.symantec.com/vip/2011/04/vipuserservices'> <vip:requestId>__request_id__</vip:requestId> <vip:onBehalfOfAccountId>__cred_id__</vip:onBehalfOfAccountId> <vip:userId>__cred_2_id__</vip:userId> <vip:credentialDetail> <vip:credentialId>__cred_3_id__</vip:credentialId> <vip:credentialType>__cred_4_id__</vip:credentialType> </vip:credentialDetail> </vip:AddCredentialRequest> </soapenv:Body></soapenv:Envelope> '@ Required = 4 } RemoveCredential = @{ Endpoint = 'https://userservices-auth.vip.symantec.com/vipuserservices/ManagementService_1_7' Template = @' <soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'> <soapenv:Body> <vip:RemoveCredentialRequest xmlns:vip='https://schemas.symantec.com/vip/2011/04/vipuserservices'> <vip:requestId>__request_id__</vip:requestId> <vip:onBehalfOfAccountId>__cred_id__</vip:onBehalfOfAccountId> <vip:userId>__cred_2_id__</vip:userId> <vip:credentialId>__cred_3_id__</vip:credentialId> <vip:credentialType>__cred_4_id__</vip:credentialType> </vip:RemoveCredentialRequest> </soapenv:Body></soapenv:Envelope> '@ Required = 4 } GetUserInfo = @{ Endpoint = 'https://userservices-auth.vip.symantec.com/vipuserservices/QueryService_1_7' Template = @' <soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'> <soapenv:Body> <vip:GetUserInfoRequest xmlns:vip='https://schemas.symantec.com/vip/2011/04/vipuserservices'> <vip:requestId>__request_id__</vip:requestId> <vip:onBehalfOfAccountId>__cred_id__</vip:onBehalfOfAccountId> <vip:userId>__cred_2_id__</vip:userId> </vip:GetUserInfoRequest> </soapenv:Body></soapenv:Envelope> '@ Required = 2 } } # endregion ------------------------------------------------------------------ # Module-scope session state (mirrors the JAR's per-request cert loading). $script:VipSession = $null # region: private helpers ---------------------------------------------------- function Get-VipRequestId { # 12-digit numeric request id, same shape as the Java tool. ('{0}{1}' -f (Get-Random -Minimum 100000 -Maximum 999999), (Get-Random -Minimum 100000 -Maximum 999999)) } function Assert-VipSession { if (-not $script:VipSession -or -not $script:VipSession.Certificate) { throw "No Symantec VIP session. Call Connect-SymantecVip first." } } function Resolve-VipEndpoint { param( [Parameter(Mandatory)] [string]$Operation, [string]$Endpoint ) if ($Endpoint) { return $Endpoint } if ($script:VipSession -and $script:VipSession.EndpointOverrides -and $script:VipSession.EndpointOverrides.ContainsKey($Operation)) { return $script:VipSession.EndpointOverrides[$Operation] } return $script:VipOperations[$Operation].Endpoint } function ConvertTo-VipHashtable { param([Parameter(Mandatory)] [System.Xml.XmlNode]$Node) $obj = [ordered]@{} foreach ($attr in $Node.Attributes) { $obj["@$($attr.LocalName)"] = $attr.Value } foreach ($child in $Node.ChildNodes) { switch ($child.NodeType) { 'Text' { return $child.Value } 'CDATA' { return $child.Value } 'Element' { $childValue = ConvertTo-VipHashtable -Node $child $name = $child.LocalName if ($obj.Contains($name)) { $existing = $obj[$name] if ($existing -is [System.Collections.Generic.List[object]]) { [void]$existing.Add($childValue) } else { # Promote fixed-size arrays / scalars to a growable list $list = [System.Collections.Generic.List[object]]::new() if ($existing -is [System.Collections.IEnumerable] -and $existing -isnot [string]) { foreach ($e in $existing) { [void]$list.Add($e) } } else { [void]$list.Add($existing) } [void]$list.Add($childValue) $obj[$name] = $list } } else { $obj[$name] = $childValue } } } } if ($obj.Count -eq 0) { return $null } return [pscustomobject]$obj } function ConvertFrom-VipSoapResponse { param( [Parameter(Mandatory)] [string]$Xml, [Parameter(Mandatory)] [string]$Endpoint, [Parameter(Mandatory)] [string]$RequestId, [Parameter(Mandatory)] [int]$HttpStatus ) [xml]$doc = $Xml $body = $null $bodyNode = $doc.GetElementsByTagName('Body', 'http://schemas.xmlsoap.org/soap/envelope/') | Select-Object -First 1 if ($bodyNode -and $bodyNode.HasChildNodes) { $inner = $bodyNode.ChildNodes | Where-Object { $_.NodeType -eq 'Element' } | Select-Object -First 1 if ($inner) { $body = ConvertTo-VipHashtable -Node $inner } } $status = $null; $statusMessage = $null; $respRequestId = $null # Try modern vipuserservices response foreach ($tag in 'status', 'statusMessage', 'requestId', 'Status', 'StatusMessage', 'RequestId') { $node = $doc.GetElementsByTagName($tag) | Select-Object -First 1 if ($node) { switch -Regex ($tag) { '^status$|^Status$' { if (-not $status) { $status = $node.InnerText } } '^statusMessage$|^StatusMessage$' { if (-not $statusMessage) { $statusMessage = $node.InnerText } } '^requestId$|^RequestId$' { if (-not $respRequestId) { $respRequestId = $node.InnerText } } } } } [pscustomobject]@{ Endpoint = $Endpoint HttpStatus = $HttpStatus StatusCode = $status StatusMessage = $statusMessage RequestId = $RequestId ResponseId = $respRequestId Body = $body RawXml = $Xml Success = ($status -eq '0000' -or $status -eq '6009') Timestamp = (Get-Date) } } function ConvertTo-VipDateTime { param([string]$Value) if (-not $Value) { return $null } $dt = [datetime]::MinValue if ([datetime]::TryParse($Value, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::RoundtripKind, [ref]$dt)) { return $dt } return $Value } function ConvertTo-VipUserObject { <# Projects a GetUserInfoResponse (as returned by Invoke-VipSoapRequest) into a flat, user-facing object. Handles zero, one or many credential bindings. Original SOAP envelope is preserved on the .Raw property. #> param( [Parameter(Mandatory)] $Response, [string]$JurisdictionHash ) $body = $Response.Body $safe = { param($node, $name) if ($node -and $node.PSObject.Properties[$name]) { $node.$name } } $bindings = @() if ($body -and ($body.PSObject.Properties['credentialBindingDetail'])) { $raw = $body.credentialBindingDetail if ($raw -is [System.Collections.IList]) { $items = $raw } else { $items = @($raw) } foreach ($c in $items) { if (-not $c) { continue } $bind = & $safe $c 'bindingDetail' $bindings += [pscustomobject]@{ CredentialId = & $safe $c 'credentialId' CredentialType = & $safe $c 'credentialType' CredentialStatus = & $safe $c 'credentialStatus' BindStatus = & $safe $bind 'bindStatus' FriendlyName = & $safe $bind 'friendlyName' LastBindTime = ConvertTo-VipDateTime (& $safe $bind 'lastBindTime') LastAuthnTime = ConvertTo-VipDateTime (& $safe $bind 'lastAuthnTime') LastAuthnId = & $safe $bind 'lastAuthnId' } } } $numBindingsRaw = & $safe $body 'numBindings' $obj = [pscustomobject]@{ JurisdictionHash = $JurisdictionHash UserId = & $safe $body 'userId' UserStatus = & $safe $body 'userStatus' UserCreationTime = ConvertTo-VipDateTime (& $safe $body 'userCreationTime') NumBindings = if ($numBindingsRaw) { [int]$numBindingsRaw } else { 0 } CredentialBindings = $bindings Status = & $safe $body 'status' StatusMessage = & $safe $body 'statusMessage' RequestId = & $safe $body 'requestId' Success = [bool]$Response.Success Raw = $Response } $obj.PSObject.TypeNames.Insert(0, 'SymantecVip.User') $obj } function ConvertTo-VipActionResult { <# Projects a write-op SOAP response (CreateUser, DeleteUser, UpdateUser, Add/RemoveCredential, Enable/Disable/Activate/Unlock token, ...) into a common, envelope-free result object. Target-specific fields (UserId, TokenId, ...) are attached via -Target so each cmdlet's result shows the identifiers relevant to that operation. #> param( [Parameter(Mandatory)] $Response, [Parameter(Mandatory)] [string]$Operation, [string]$JurisdictionHash, [System.Collections.IDictionary]$Target ) $get = { param($name, $default) if ($Response.PSObject.Properties[$name]) { $Response.$name } else { $default } } $props = [ordered]@{ Operation = $Operation JurisdictionHash = $JurisdictionHash } if ($Target) { foreach ($k in $Target.Keys) { $props[$k] = $Target[$k] } } $props.StatusCode = & $get 'StatusCode' $null $props.StatusMessage = & $get 'StatusMessage' $null $props.Success = [bool](& $get 'Success' $false) $props.RequestId = & $get 'RequestId' $null $props.Timestamp = & $get 'Timestamp' (Get-Date) $props.Raw = $Response $out = [pscustomobject]$props $out.PSObject.TypeNames.Insert(0, 'SymantecVip.ActionResult') $out } function Export-VipUserBackup { <# Fetches a user's current state via GetUserInfo and writes one CSV (one row per credential binding, or one blank-cred row if the user has none). Returns the resolved absolute path to the file it wrote. Throws if the user cannot be fetched or has a non-success status, so callers can abort the destructive action safely. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$JurisdictionHash, [Parameter(Mandatory)] [string]$UserId, [Parameter(Mandatory)] [string]$Directory ) $u = Get-SymantecVipUser -JurisdictionHash $JurisdictionHash -UserId $UserId if (-not $u.Success) { throw "GetUserInfo for '$UserId' returned status $($u.Status) '$($u.StatusMessage)' - refusing to back up an unreadable user." } if (-not (Test-Path -LiteralPath $Directory -PathType Container)) { [void](New-Item -ItemType Directory -Force -Path $Directory -ErrorAction Stop) } $dir = (Resolve-Path -LiteralPath $Directory).ProviderPath $safeId = ($UserId -replace '[^\w.\-]', '_') $file = Join-Path $dir ("{0}-{1}.csv" -f $safeId, (Get-Date -Format 'yyyyMMdd-HHmmss')) $rows = @() $baseFields = [ordered]@{ JurisdictionHash = $JurisdictionHash UserId = $u.UserId UserStatus = $u.UserStatus UserCreationTime = $u.UserCreationTime NumBindings = $u.NumBindings BackedUpAt = (Get-Date).ToString('o') } if ($u.CredentialBindings -and @($u.CredentialBindings).Count -gt 0) { foreach ($c in $u.CredentialBindings) { $row = [ordered]@{} foreach ($k in $baseFields.Keys) { $row[$k] = $baseFields[$k] } $row.CredentialId = $c.CredentialId $row.CredentialType = $c.CredentialType $row.CredentialStatus = $c.CredentialStatus $row.BindStatus = $c.BindStatus $row.FriendlyName = $c.FriendlyName $row.LastBindTime = $c.LastBindTime $row.LastAuthnTime = $c.LastAuthnTime $row.LastAuthnId = $c.LastAuthnId $rows += [pscustomobject]$row } } else { $row = [ordered]@{} foreach ($k in $baseFields.Keys) { $row[$k] = $baseFields[$k] } foreach ($k in 'CredentialId','CredentialType','CredentialStatus','BindStatus','FriendlyName','LastBindTime','LastAuthnTime','LastAuthnId') { $row[$k] = $null } $rows += [pscustomobject]$row } $rows | Export-Csv -LiteralPath $file -NoTypeInformation -Encoding UTF8 $file } function New-VipSoapBody { <# Renders a SOAP body from the templates extracted from the JAR by substituting __request_id__ and the __cred_*_id__ placeholders. Exposed for testability. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Operation, [Parameter(Mandatory)] [string[]]$Properties, [string]$RequestId ) $spec = $script:VipOperations[$Operation] if (-not $spec) { throw "Unknown operation '$Operation'." } if ($Properties.Count -lt $spec.Required) { throw "Operation '$Operation' requires $($spec.Required) properties, only $($Properties.Count) supplied." } if (-not $RequestId) { $RequestId = Get-VipRequestId } $body = $spec.Template $body = $body -replace '__request_id__', $RequestId for ($i = 0; $i -lt $Properties.Count; $i++) { $token = if ($i -eq 0) { '__cred_id__' } else { '__cred_{0}_id__' -f ($i + 1) } $body = $body.Replace($token, [System.Security.SecurityElement]::Escape([string]$Properties[$i])) } [pscustomobject]@{ Operation = $Operation RequestId = $RequestId Endpoint = $spec.Endpoint Body = $body } } function Invoke-VipSoapRequest { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Operation, [Parameter(Mandatory)] [string[]]$Properties, [string]$Endpoint, [string]$SoapAction = '' ) Assert-VipSession $url = Resolve-VipEndpoint -Operation $Operation -Endpoint $Endpoint $rendered = New-VipSoapBody -Operation $Operation -Properties $Properties $requestId = $rendered.RequestId $body = $rendered.Body Write-Verbose "[$Operation] POST $url (requestId=$requestId)" Write-Debug "[$Operation] Request body: $body" # Belt-and-suspenders TLS 1.2/1.3 for Windows PowerShell 5.1 (WinPS defaults # to TLS 1.0/1.1 which Symantec VIP rejects, producing an opaque # "SSL connection could not be established" error). try { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 } catch { } $handler = [System.Net.Http.HttpClientHandler]::new() try { $tls12 = [System.Security.Authentication.SslProtocols]::Tls12 $tls13 = [System.Security.Authentication.SslProtocols]::Tls13 $handler.SslProtocols = $tls12 -bor $tls13 } catch { $handler.SslProtocols = [System.Security.Authentication.SslProtocols]::Tls12 } [void]$handler.ClientCertificates.Add($script:VipSession.Certificate) if ($script:VipSession.SkipCertificateCheck) { $handler.ServerCertificateCustomValidationCallback = [System.Net.Http.HttpClientHandler]::DangerousAcceptAnyServerCertificateValidator } $client = [System.Net.Http.HttpClient]::new($handler) $client.Timeout = [TimeSpan]::FromSeconds($script:VipSession.TimeoutSeconds) try { $content = [System.Net.Http.StringContent]::new($body, [System.Text.Encoding]::UTF8, 'text/xml') if ($SoapAction) { [void]$content.Headers.TryAddWithoutValidation('SOAPAction', $SoapAction) } try { $response = $client.PostAsync($url, $content).GetAwaiter().GetResult() } catch [System.AggregateException] { throw $_.Exception.Flatten().InnerException } catch { $ex = $_.Exception while ($ex.InnerException) { $ex = $ex.InnerException } throw $ex } $respText = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult() $httpStatus = [int]$response.StatusCode Write-Debug "[$Operation] HTTP $httpStatus Response: $respText" return ConvertFrom-VipSoapResponse -Xml $respText -Endpoint $url ` -RequestId $requestId -HttpStatus $httpStatus } finally { $client.Dispose() $handler.Dispose() } } # endregion ------------------------------------------------------------------ # region: public functions --------------------------------------------------- function Connect-SymantecVip { <# .SYNOPSIS Opens a Symantec VIP session using a PKCS#12 client certificate. .DESCRIPTION Loads the client certificate that will be presented for mTLS on every subsequent call. Certificates are held in memory only. Use Disconnect-SymantecVip to clear the session. The Java tool uses a PKCS12 keystore (see com.tangynt.shared.ApiInterface setUpSSL). This function accepts a .p12 / .pfx file at -CertificatePath with -CertificatePassword, or a pre-loaded X509Certificate2 via -Certificate. .EXAMPLE $pw = Read-Host -AsSecureString -Prompt 'PFX password' Connect-SymantecVip -CertificatePath 'C:\vip\vip-cert.p12' -CertificatePassword $pw #> [CmdletBinding(DefaultParameterSetName = 'Path')] param( [Parameter(Mandatory, ParameterSetName = 'Path')] [ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })] [string]$CertificatePath, [Parameter(Mandatory, ParameterSetName = 'Path')] [securestring]$CertificatePassword, [Parameter(Mandatory, ParameterSetName = 'Cert')] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate, [hashtable]$EndpointOverrides, [int]$TimeoutSeconds = 60, [switch]$SkipCertificateCheck ) if ($PSCmdlet.ParameterSetName -eq 'Path') { # On Windows SChannel cannot consume EphemeralKeySet private keys for # client-cert TLS (SEC_E_UNKNOWN_CREDENTIALS). Persist the key on # Windows; only fall back to Ephemeral on non-Windows. $isWindows = if ($PSVersionTable.PSEdition -eq 'Desktop') { $true } else { [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) } $baseFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::UserKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable $flagOptions = @() $flagOptions += ($baseFlags -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet) $flagOptions += $baseFlags if (-not $isWindows) { try { $ephem = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet $flagOptions += ($baseFlags -bor $ephem) } catch { } } $pfxPath = (Resolve-Path -LiteralPath $CertificatePath).ProviderPath $lastError = $null foreach ($flags in $flagOptions) { try { $Certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new( $pfxPath, $CertificatePassword, $flags) $lastError = $null break } catch { $lastError = $_.Exception } } if ($lastError) { throw "Could not load PKCS#12 certificate '$CertificatePath': $($lastError.Message)" } } if (-not $Certificate.HasPrivateKey) { throw "Certificate '$($Certificate.Subject)' has no private key; mTLS to Symantec VIP will fail." } # Verify the private key is actually usable in this process. On some hosts # the PFX loads but the CNG/CAPI key handle is unreachable, which shows up # later as a generic "SSL connection could not be established" error. try { $null = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate) } catch { Write-Warning "Certificate private key present but not accessible: $($_.Exception.Message). The TLS handshake will likely fail." } $script:VipSession = [pscustomobject]@{ Certificate = $Certificate EndpointOverrides = $EndpointOverrides TimeoutSeconds = $TimeoutSeconds SkipCertificateCheck = [bool]$SkipCertificateCheck ConnectedAt = Get-Date Subject = $Certificate.Subject Thumbprint = $Certificate.Thumbprint Expires = $Certificate.NotAfter } Write-Verbose "Symantec VIP session opened with certificate $($Certificate.Subject) (thumb $($Certificate.Thumbprint), expires $($Certificate.NotAfter))." [pscustomobject]@{ Subject = $Certificate.Subject Thumbprint = $Certificate.Thumbprint Expires = $Certificate.NotAfter ConnectedAt = $script:VipSession.ConnectedAt } } function Disconnect-SymantecVip { <# .SYNOPSIS Clears the in-memory Symantec VIP session and deletes the persisted private key container that Connect-SymantecVip created for SChannel. #> [CmdletBinding()] param() if ($script:VipSession -and $script:VipSession.Certificate) { $cert = $script:VipSession.Certificate try { $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert) if ($rsa -and $rsa.Key -and $rsa.Key.UniqueName) { try { $rsa.Key.Delete() } catch { } } if ($rsa) { $rsa.Dispose() } } catch { } try { $cert.Dispose() } catch { } } $script:VipSession = $null Write-Verbose "Symantec VIP session closed." } function Get-SymantecVipContext { <# .SYNOPSIS Returns metadata about the current VIP session (no private key material). #> [CmdletBinding()] param() if (-not $script:VipSession) { return } $script:VipSession | Select-Object Subject, Thumbprint, Expires, ConnectedAt, TimeoutSeconds, SkipCertificateCheck, EndpointOverrides } function Get-SymantecVipUser { <# .SYNOPSIS Calls GetUserInfo on the VIP QueryService and returns a friendly user object. Use -Raw for the full SOAP envelope response. .PARAMETER JurisdictionHash The Symantec VIP tenant / jurisdiction hash (numeric). Also known as onBehalfOfAccountId / authorizerAccountId in the raw SOAP schema. .PARAMETER UserId The VIP userId to look up. .PARAMETER Raw Return the untouched SOAP envelope object (Endpoint, HttpStatus, RawXml, ...) instead of the flattened user object. .OUTPUTS PSCustomObject with JurisdictionHash, UserId, UserStatus, UserCreationTime, NumBindings, CredentialBindings, Status, StatusMessage. #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] [Alias('OnBehalfOfAccountId', 'AuthorizerAccountId')] [string]$JurisdictionHash, [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)] [string]$UserId, [switch]$Raw ) process { $resp = Invoke-VipSoapRequest -Operation GetUserInfo -Properties $JurisdictionHash, $UserId if ($Raw) { return $resp } ConvertTo-VipUserObject -Response $resp -JurisdictionHash $JurisdictionHash } } function New-SymantecVipUser { <# .SYNOPSIS Calls CreateUser on the VIP ManagementService, optionally followed by AddCredential in the same call. Returns one SymantecVip.ActionResult per SOAP step (two objects when a credential is bound); use -Raw for the full SOAP envelopes. .PARAMETER CredentialId Optional. If provided together with -CredentialType, binds that credential to the user immediately after CreateUser succeeds. .PARAMETER CredentialType Required whenever -CredentialId is used. Same enum as Add-SymantecVipCredential. .EXAMPLE New-SymantecVipUser -JurisdictionHash $jh -UserId 'jdoe' .EXAMPLE New-SymantecVipUser -JurisdictionHash $jh -UserId 'jdoe' ` -CredentialId 'VSST12345678' -CredentialType STANDARD_OTP #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'User')] param( [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] [Alias('OnBehalfOfAccountId', 'AuthorizerAccountId')] [string]$JurisdictionHash, [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)] [string]$UserId, [Parameter(Mandatory, ParameterSetName = 'UserWithCredential', ValueFromPipelineByPropertyName)] [string]$CredentialId, [Parameter(Mandatory, ParameterSetName = 'UserWithCredential', ValueFromPipelineByPropertyName)] [ValidateSet('STANDARD_OTP','SMS_OTP','VOICE_OTP','SERVICE_OTP','CERTIFICATE')] [string]$CredentialType, [switch]$Raw ) process { $withCred = $PSCmdlet.ParameterSetName -eq 'UserWithCredential' $action = if ($withCred) { "CreateUser + AddCredential ($CredentialType)" } else { 'CreateUser' } if (-not $PSCmdlet.ShouldProcess("$UserId (tenant $JurisdictionHash)", $action)) { return } $resp = Invoke-VipSoapRequest -Operation CreateUser -Properties $JurisdictionHash, $UserId if ($Raw) { $resp } else { ConvertTo-VipActionResult -Response $resp -Operation CreateUser ` -JurisdictionHash $JurisdictionHash -Target ([ordered]@{ UserId = $UserId }) } if (-not $withCred) { return } if (-not $resp.Success) { Write-Warning "Skipping AddCredential for '$UserId' because CreateUser failed (StatusCode $($resp.StatusCode) $($resp.StatusMessage))." return } Add-SymantecVipCredential -JurisdictionHash $JurisdictionHash -UserId $UserId ` -CredentialId $CredentialId -CredentialType $CredentialType ` -Confirm:$false -Raw:$Raw } } function Set-SymantecVipUser { <# .SYNOPSIS Calls UpdateUser on the VIP ManagementService. .DESCRIPTION UpdateUser can rename a userId and/or change its status. Provide the current userId in -UserId, then the target values in -NewUserId and -NewUserStatus. Valid VIP user statuses include ENABLED and DISABLED. Returns a friendly SymantecVip.ActionResult; use -Raw for the full SOAP envelope. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [Alias('OnBehalfOfAccountId', 'AuthorizerAccountId')] [string]$JurisdictionHash, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$UserId, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$NewUserId, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateSet('ENABLED', 'DISABLED')] [string]$NewUserStatus, [switch]$Raw ) process { if ($PSCmdlet.ShouldProcess("$UserId -> $NewUserId ($NewUserStatus)", 'UpdateUser')) { $resp = Invoke-VipSoapRequest -Operation UpdateUser ` -Properties $JurisdictionHash, $UserId, $NewUserId, $NewUserStatus if ($Raw) { return $resp } ConvertTo-VipActionResult -Response $resp -Operation UpdateUser ` -JurisdictionHash $JurisdictionHash -Target ([ordered]@{ UserId = $UserId NewUserId = $NewUserId NewUserStatus = $NewUserStatus }) } } } function Remove-SymantecVipUser { <# .SYNOPSIS Calls DeleteUser on the VIP ManagementService. Returns a friendly SymantecVip.ActionResult; use -Raw for the full SOAP envelope. .DESCRIPTION The user is always backed up to CSV before deletion so the whole record (user + all credential bindings) can be restored later with Restore-SymantecVipUser. Use -BackupPath to control where the CSV is written (defaults to '.\vip-user-backups' under the current directory). Use -SkipBackup to explicitly bypass the safety net and delete without a snapshot. .PARAMETER BackupPath Directory that the pre-deletion snapshot is written into. Ignored when -SkipBackup is present. Default: '.\vip-user-backups'. .PARAMETER SkipBackup Skip the pre-deletion snapshot entirely. Use only when you already have a backup or the user has no bound credentials worth preserving. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param( [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] [Alias('OnBehalfOfAccountId', 'AuthorizerAccountId')] [string]$JurisdictionHash, [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)] [string]$UserId, [Parameter(ValueFromPipelineByPropertyName)] [string]$BackupPath = '.\vip-user-backups', [Parameter(ValueFromPipelineByPropertyName)] [switch]$SkipBackup, [switch]$Raw ) process { if ($SkipBackup -and $PSBoundParameters.ContainsKey('BackupPath')) { Write-Warning "-BackupPath is ignored when -SkipBackup is present." } $shouldProcessDesc = if ($SkipBackup) { "$UserId (tenant $JurisdictionHash) [no backup]" } else { "$UserId (tenant $JurisdictionHash) [backup -> $BackupPath]" } if (-not $PSCmdlet.ShouldProcess($shouldProcessDesc, 'DeleteUser')) { return } $backupFile = $null if (-not $SkipBackup) { try { $backupFile = Export-VipUserBackup -JurisdictionHash $JurisdictionHash ` -UserId $UserId -Directory $BackupPath Write-Verbose "Backed up '$UserId' to '$backupFile' before deletion." } catch { Write-Warning "Aborting DeleteUser for '$UserId': backup failed - $($_.Exception.Message). Pass -SkipBackup to delete anyway." # Emit a real result so bulk callers see the skipped row instead # of crashing on $null; type-tagged as SymantecVip.ActionResult # so the table view renders it consistently. $abort = [pscustomobject]@{ Operation = 'DeleteUser' JurisdictionHash = $JurisdictionHash UserId = $UserId StatusCode = 'BACKUP_ABORTED' StatusMessage = "Backup failed: $($_.Exception.Message.TrimEnd('.'))" Success = $false RequestId = $null Timestamp = (Get-Date) Raw = $null BackupFile = $null } $abort.PSObject.TypeNames.Insert(0, 'SymantecVip.ActionResult') return $abort } } $resp = Invoke-VipSoapRequest -Operation DeleteUser -Properties $JurisdictionHash, $UserId if ($Raw) { if ($backupFile) { $resp | Add-Member -NotePropertyName BackupFile -NotePropertyValue $backupFile -Force } return $resp } $result = ConvertTo-VipActionResult -Response $resp -Operation DeleteUser ` -JurisdictionHash $JurisdictionHash -Target ([ordered]@{ UserId = $UserId }) if ($backupFile) { $result | Add-Member -NotePropertyName BackupFile -NotePropertyValue $backupFile -Force } $result } } function Restore-SymantecVipUser { <# .SYNOPSIS Rebuilds a VIP user (and re-binds their credentials) from a backup CSV produced by Remove-SymantecVipUser -BackupPath / Invoke-SymantecVipBulkOperation. .DESCRIPTION Reads the CSV, calls CreateUser once, then AddCredential for each row that has a CredentialId / CredentialType. Returns one SymantecVip.ActionResult per SOAP step so the restore trail is visible. .PARAMETER Path A single backup CSV file (as produced by the backup helper). .PARAMETER Directory A directory containing backup CSVs; every *.csv inside is restored. .PARAMETER JurisdictionHash Overrides the tenant recorded in the CSV. Useful when restoring into a different tenant than the one the user was backed up from. .EXAMPLE Restore-SymantecVipUser -Path .\backups\jdoe-20260101-000000.csv .EXAMPLE Restore-SymantecVipUser -Directory .\backups -WhatIf #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'File')] param( [Parameter(Mandatory, Position = 0, ParameterSetName = 'File', ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('FullName', 'PSPath')] [string]$Path, [Parameter(Mandatory, ParameterSetName = 'Directory')] [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })] [string]$Directory, [Parameter(ValueFromPipelineByPropertyName)] [Alias('OnBehalfOfAccountId', 'AuthorizerAccountId')] [string]$JurisdictionHash, [switch]$Raw ) process { if ($PSCmdlet.ParameterSetName -eq 'Directory') { $files = Get-ChildItem -LiteralPath $Directory -Filter *.csv -File | Sort-Object Name foreach ($f in $files) { Restore-SymantecVipUser -Path $f.FullName -JurisdictionHash $JurisdictionHash -Raw:$Raw } return } if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "Backup file '$Path' not found." } $rows = @(Import-Csv -LiteralPath $Path) if ($rows.Count -eq 0) { Write-Warning "Backup file '$Path' has no rows; skipping." return } $u = $rows[0] $jh = if ($JurisdictionHash) { $JurisdictionHash } else { $u.JurisdictionHash } $uid = $u.UserId if (-not $jh -or -not $uid) { throw "Backup file '$Path' is missing JurisdictionHash or UserId." } $credRows = @($rows | Where-Object { $_.CredentialId -and $_.CredentialType }) $action = if ($credRows.Count -gt 0) { "CreateUser + $($credRows.Count) x AddCredential from '$([IO.Path]::GetFileName($Path))'" } else { "CreateUser from '$([IO.Path]::GetFileName($Path))'" } if (-not $PSCmdlet.ShouldProcess("$uid (tenant $jh)", $action)) { return } $createResp = Invoke-VipSoapRequest -Operation CreateUser -Properties $jh, $uid if ($Raw) { $createResp } else { ConvertTo-VipActionResult -Response $createResp -Operation CreateUser ` -JurisdictionHash $jh -Target ([ordered]@{ UserId = $uid; RestoredFrom = $Path }) } if (-not $createResp.Success) { Write-Warning "CreateUser failed for '$uid' (StatusCode $($createResp.StatusCode) $($createResp.StatusMessage)); skipping credential restore." return } foreach ($row in $credRows) { Add-SymantecVipCredential -JurisdictionHash $jh -UserId $uid ` -CredentialId $row.CredentialId -CredentialType $row.CredentialType ` -Confirm:$false -Raw:$Raw } } } function Add-SymantecVipCredential { <# .SYNOPSIS Binds a credential to a VIP user. Returns a friendly SymantecVip.ActionResult; use -Raw for the full SOAP envelope. .PARAMETER CredentialType VIP User Services credentialType enum. Must be one of: STANDARD_OTP - VIP hardware / VIP Access soft tokens (VSST*, SYMC*) SMS_OTP - SMS-delivered OTP VOICE_OTP - Voice-call OTP SERVICE_OTP - Service-provisioned OTP CERTIFICATE - Client certificate #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [Alias('OnBehalfOfAccountId', 'AuthorizerAccountId')] [string]$JurisdictionHash, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$UserId, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$CredentialId, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateSet('STANDARD_OTP','SMS_OTP','VOICE_OTP','SERVICE_OTP','CERTIFICATE')] [string]$CredentialType, [switch]$Raw ) process { if ($PSCmdlet.ShouldProcess("$UserId <- $CredentialId ($CredentialType)", 'AddCredential')) { $resp = Invoke-VipSoapRequest -Operation AddCredential ` -Properties $JurisdictionHash, $UserId, $CredentialId, $CredentialType if ($Raw) { return $resp } ConvertTo-VipActionResult -Response $resp -Operation AddCredential ` -JurisdictionHash $JurisdictionHash -Target ([ordered]@{ UserId = $UserId CredentialId = $CredentialId CredentialType = $CredentialType }) } } } function Remove-SymantecVipCredential { <# .SYNOPSIS Unbinds a credential from a VIP user. Returns a friendly SymantecVip.ActionResult; use -Raw for the full SOAP envelope. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [Alias('OnBehalfOfAccountId', 'AuthorizerAccountId')] [string]$JurisdictionHash, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$UserId, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$CredentialId, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateSet('STANDARD_OTP','SMS_OTP','VOICE_OTP','SERVICE_OTP','CERTIFICATE')] [string]$CredentialType, [switch]$Raw ) process { if ($PSCmdlet.ShouldProcess("$UserId x $CredentialId ($CredentialType)", 'RemoveCredential')) { $resp = Invoke-VipSoapRequest -Operation RemoveCredential ` -Properties $JurisdictionHash, $UserId, $CredentialId, $CredentialType if ($Raw) { return $resp } ConvertTo-VipActionResult -Response $resp -Operation RemoveCredential ` -JurisdictionHash $JurisdictionHash -Target ([ordered]@{ UserId = $UserId CredentialId = $CredentialId CredentialType = $CredentialType }) } } } function Get-SymantecVipTokenInformation { <# .SYNOPSIS Retrieves token metadata via the legacy VIP management SOAP endpoint. #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] [Alias('OnBehalfOfAccountId', 'AuthorizerAccountId')] [string]$JurisdictionHash, [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)] [string]$TokenId ) process { Invoke-VipSoapRequest -Operation GetTokenInformation ` -Properties $JurisdictionHash, $TokenId } } function Enable-SymantecVipToken { [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] [Alias('OnBehalfOfAccountId', 'AuthorizerAccountId')] [string]$JurisdictionHash, [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)] [string]$TokenId, [switch]$Raw ) process { if ($PSCmdlet.ShouldProcess($TokenId, 'EnableToken')) { $resp = Invoke-VipSoapRequest -Operation EnableToken -Properties $JurisdictionHash, $TokenId if ($Raw) { return $resp } ConvertTo-VipActionResult -Response $resp -Operation EnableToken ` -JurisdictionHash $JurisdictionHash -Target ([ordered]@{ TokenId = $TokenId }) } } } function Disable-SymantecVipToken { [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] [Alias('OnBehalfOfAccountId', 'AuthorizerAccountId')] [string]$JurisdictionHash, [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)] [string]$TokenId, [switch]$Raw ) process { if ($PSCmdlet.ShouldProcess($TokenId, 'DisableToken')) { $resp = Invoke-VipSoapRequest -Operation DisableToken -Properties $JurisdictionHash, $TokenId if ($Raw) { return $resp } ConvertTo-VipActionResult -Response $resp -Operation DisableToken ` -JurisdictionHash $JurisdictionHash -Target ([ordered]@{ TokenId = $TokenId }) } } } function Approve-SymantecVipToken { <# .SYNOPSIS Activates a newly-issued VIP token (ActivateToken). Alias: Activate-SymantecVipToken. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] [Alias('OnBehalfOfAccountId', 'AuthorizerAccountId')] [string]$JurisdictionHash, [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)] [string]$TokenId, [switch]$Raw ) process { if ($PSCmdlet.ShouldProcess($TokenId, 'ActivateToken')) { $resp = Invoke-VipSoapRequest -Operation ActivateToken -Properties $JurisdictionHash, $TokenId if ($Raw) { return $resp } ConvertTo-VipActionResult -Response $resp -Operation ActivateToken ` -JurisdictionHash $JurisdictionHash -Target ([ordered]@{ TokenId = $TokenId }) } } } function Unlock-SymantecVipToken { <# .SYNOPSIS Unlocks a VIP token. Use -Type SMS or Voice to unlock the phone-based variants that the JAR exposed as UnlockSms / UnlockVoice. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] [Alias('OnBehalfOfAccountId', 'AuthorizerAccountId')] [string]$JurisdictionHash, [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)] [string]$TokenId, [Parameter(ValueFromPipelineByPropertyName)] [ValidateSet('Default', 'SMS', 'Voice')] [string]$Type = 'Default', [switch]$Raw ) process { $op = switch ($Type) { 'SMS' { 'UnlockSmsToken' } 'Voice' { 'UnlockVoiceToken' } default { 'UnlockToken' } } if ($PSCmdlet.ShouldProcess($TokenId, $op)) { $resp = Invoke-VipSoapRequest -Operation $op -Properties $JurisdictionHash, $TokenId if ($Raw) { return $resp } $target = [ordered]@{ TokenId = $TokenId } if ($Type -ne 'Default') { $target.Type = $Type } ConvertTo-VipActionResult -Response $resp -Operation $op ` -JurisdictionHash $JurisdictionHash -Target $target } } } function Invoke-SymantecVipBulkOperation { <# .SYNOPSIS Runs one VIP operation against a set of rows piped in or read from CSV. .DESCRIPTION Convenience wrapper for CSV-driven bulk jobs. Rows must contain columns matching the parameter names of the corresponding cmdlet (case insensitive). Errors are captured per-row so a single failure does not abort the batch. .PARAMETER Operation Which VIP operation to perform for each row. .PARAMETER InputObject Rows to process; typically the result of Import-Csv. .PARAMETER Path CSV file to import (alternative to piping -InputObject). .PARAMETER ThrottleSeconds Optional delay (seconds) between rows to avoid rate limits. .PARAMETER BackupPath Only used with -Operation DeleteUser. Directory where a per-user CSV snapshot is written before deletion, so Restore-SymantecVipUser can rebuild the user later. Defaults to '.\vip-user-backups'. .PARAMETER SkipBackup Only used with -Operation DeleteUser. Skip the pre-deletion backup. .EXAMPLE Import-Csv .\users.csv | Invoke-SymantecVipBulkOperation -Operation DeleteUser ` -BackupPath .\vip-user-backups -OutVariable results #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High', DefaultParameterSetName = 'Pipeline')] param( [Parameter(Mandatory, Position = 0)] [ValidateSet('CreateUser', 'DeleteUser', 'UpdateUser', 'GetUserInfo', 'AddCredential', 'RemoveCredential', 'ActivateToken', 'EnableToken', 'DisableToken', 'UnlockToken', 'UnlockSmsToken', 'UnlockVoiceToken', 'GetTokenInformation')] [string]$Operation, [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Pipeline')] [psobject[]]$InputObject, [Parameter(Mandatory, ParameterSetName = 'File')] [ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })] [string]$Path, [double]$ThrottleSeconds = 0, [string]$BackupPath, [switch]$SkipBackup ) begin { Assert-VipSession if ($PSCmdlet.ParameterSetName -eq 'File') { $InputObject = @(Import-Csv -LiteralPath $Path) } if (($BackupPath -or $SkipBackup) -and $Operation -ne 'DeleteUser') { Write-Warning "-BackupPath / -SkipBackup are only used with -Operation DeleteUser; they will be ignored for '$Operation'." } $rowIndex = 0 } process { foreach ($row in $InputObject) { $rowIndex++ $target = "row #$rowIndex" $ht = @{} foreach ($p in $row.PSObject.Properties) { $ht[$p.Name] = $p.Value } if ($Operation -eq 'DeleteUser') { if ($BackupPath -and -not $ht.ContainsKey('BackupPath')) { $ht['BackupPath'] = $BackupPath } if ($SkipBackup -and -not $ht.ContainsKey('SkipBackup')) { $ht['SkipBackup'] = $true } } if (-not $PSCmdlet.ShouldProcess($target, $Operation)) { continue } $result = $null $errText = $null try { switch ($Operation) { 'CreateUser' { $result = New-SymantecVipUser @ht -Confirm:$false } 'DeleteUser' { $result = Remove-SymantecVipUser @ht -Confirm:$false } 'UpdateUser' { $result = Set-SymantecVipUser @ht -Confirm:$false } 'GetUserInfo' { $result = Get-SymantecVipUser @ht } 'AddCredential' { $result = Add-SymantecVipCredential @ht -Confirm:$false } 'RemoveCredential' { $result = Remove-SymantecVipCredential @ht -Confirm:$false } 'GetTokenInformation'{ $result = Get-SymantecVipTokenInformation @ht } 'EnableToken' { $result = Enable-SymantecVipToken @ht -Confirm:$false } 'DisableToken' { $result = Disable-SymantecVipToken @ht -Confirm:$false } 'ActivateToken' { $result = Approve-SymantecVipToken @ht -Confirm:$false } 'UnlockToken' { $result = Unlock-SymantecVipToken @ht -Confirm:$false } 'UnlockSmsToken' { $result = Unlock-SymantecVipToken @ht -Type SMS -Confirm:$false } 'UnlockVoiceToken' { $result = Unlock-SymantecVipToken @ht -Type Voice -Confirm:$false } } } catch { $errText = $_.Exception.Message Write-Warning "[$Operation row #$rowIndex] $errText" } # Defensive readers so a $null result or missing property never # aborts the batch under Set-StrictMode. $get = { param($obj, $name, $default = $null) if ($obj -and $obj.PSObject.Properties[$name]) { $obj.$name } else { $default } } [pscustomobject]@{ Row = $rowIndex Operation = $Operation Input = [pscustomobject]$ht StatusCode = & $get $result 'StatusCode' StatusMessage = & $get $result 'StatusMessage' Success = [bool](& $get $result 'Success' $false) RequestId = & $get $result 'RequestId' BackupFile = & $get $result 'BackupFile' Error = $errText Result = $result } if ($ThrottleSeconds -gt 0) { Start-Sleep -Milliseconds ([int]($ThrottleSeconds * 1000)) } } } } function Test-SymantecVipConnection { <# .SYNOPSIS Diagnoses why an mTLS call to Symantec VIP is failing. .DESCRIPTION Runs a series of local checks (TLS version, proxy, private-key accessibility) and then attempts a raw TLS handshake against the selected Symantec VIP endpoint. Prints the *actual* inner exception instead of the generic "SSL connection could not be established" message. .PARAMETER Endpoint Which endpoint to probe. Defaults to the User Services host used by Get-SymantecVipUser / New-SymantecVipUser etc. .EXAMPLE Test-SymantecVipConnection -Verbose #> [CmdletBinding()] param( [ValidateSet('UserServices', 'Legacy')] [string]$Endpoint = 'UserServices' ) Assert-VipSession $host = if ($Endpoint -eq 'Legacy') { 'services-auth.vip.symantec.com' } else { 'userservices-auth.vip.symantec.com' } $port = 443 $findings = [ordered]@{} # 1. PS + framework versions. $findings.PowerShellVersion = $PSVersionTable.PSVersion.ToString() $findings.PSEdition = $PSVersionTable.PSEdition $findings.ClrVersion = [System.Environment]::Version.ToString() # 2. TLS defaults. try { $findings.ServicePointManagerProtocols = [System.Net.ServicePointManager]::SecurityProtocol.ToString() } catch { $findings.ServicePointManagerProtocols = 'unavailable' } # 3. Certificate summary and private-key sanity. $cert = $script:VipSession.Certificate $findings.CertSubject = $cert.Subject $findings.CertIssuer = $cert.Issuer $findings.CertThumbprint = $cert.Thumbprint $findings.CertNotAfter = $cert.NotAfter $findings.CertHasKey = $cert.HasPrivateKey try { $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert) $findings.PrivateKeyUsable = [bool]$rsa if ($rsa) { $rsa.Dispose() } } catch { $findings.PrivateKeyUsable = "no: $($_.Exception.Message)" } # 4. Proxy in effect for the target URL. try { $proxy = [System.Net.WebRequest]::GetSystemWebProxy().GetProxy([Uri]("https://$host")) $findings.SystemProxy = if ($proxy.AbsoluteUri -eq "https://$host/") { 'none' } else { $proxy.AbsoluteUri } } catch { $findings.SystemProxy = 'unknown' } # 5. Raw TCP + TLS handshake, no HTTP payload. Reveals the true handshake # error (bad client cert, TLS version mismatch, proxy MITM, ...). $tcp = $null; $ssl = $null try { $tcp = [System.Net.Sockets.TcpClient]::new() $tcp.SendTimeout = 10000 $tcp.ReceiveTimeout = 10000 $tcp.Connect($host, $port) $findings.TcpConnect = 'ok' $validate = if ($script:VipSession.SkipCertificateCheck) { [System.Net.Security.RemoteCertificateValidationCallback]{ param($s,$c,$ch,$e) $true } } else { [System.Net.Security.RemoteCertificateValidationCallback]{ param($s,$c,$ch,$e) if ($e -ne [System.Net.Security.SslPolicyErrors]::None) { Write-Verbose "Server cert policy errors: $e" } $e -eq [System.Net.Security.SslPolicyErrors]::None } } $ssl = [System.Net.Security.SslStream]::new($tcp.GetStream(), $false, $validate) $clientCerts = [System.Security.Cryptography.X509Certificates.X509CertificateCollection]::new() [void]$clientCerts.Add($cert) $tls = try { [System.Security.Authentication.SslProtocols]::Tls12 -bor [System.Security.Authentication.SslProtocols]::Tls13 } catch { [System.Security.Authentication.SslProtocols]::Tls12 } $ssl.AuthenticateAsClient($host, $clientCerts, $tls, $false) $findings.TlsHandshake = 'ok' $findings.NegotiatedTls = $ssl.SslProtocol.ToString() $findings.NegotiatedCipher = $ssl.CipherAlgorithm.ToString() $findings.ServerCertSubject = $ssl.RemoteCertificate.Subject } catch { $ex = $_.Exception while ($ex.InnerException) { $ex = $ex.InnerException } $findings.TlsHandshake = "failed: $($ex.GetType().Name): $($ex.Message)" } finally { if ($ssl) { $ssl.Dispose() } if ($tcp) { $tcp.Dispose() } } [pscustomobject]$findings } # endregion ------------------------------------------------------------------ Set-Alias -Name Activate-SymantecVipToken -Value Approve-SymantecVipToken -Force Set-Alias -Name Get-SymantecVipTokenInfo -Value Get-SymantecVipTokenInformation -Force Set-Alias -Name Update-SymantecVipUser -Value Set-SymantecVipUser -Force Export-ModuleMember -Function Connect-SymantecVip, Disconnect-SymantecVip, Get-SymantecVipContext, Get-SymantecVipUser, New-SymantecVipUser, Set-SymantecVipUser, Remove-SymantecVipUser, Restore-SymantecVipUser, Add-SymantecVipCredential, Remove-SymantecVipCredential, Get-SymantecVipTokenInformation, Enable-SymantecVipToken, Disable-SymantecVipToken, Approve-SymantecVipToken, Unlock-SymantecVipToken, Invoke-SymantecVipBulkOperation, Test-SymantecVipConnection ` -Alias Activate-SymantecVipToken, Get-SymantecVipTokenInfo, Update-SymantecVipUser |