Understanding IDIRA (CyberArk) TPC Plugins: Process.ini, Prompts.ini, PowerShell, and the State Machine

When I first started looking at IDIRA (CyberArk) Terminal Plugin Controller (TPC) plugins, I found the concept a little confusing.

There was a Process.ini, a Prompts.ini, transitions, states, conditions, FAIL(), PowerShell output… and somehow all of these pieces were supposed to work together to perform a password change.

Once I understood one key concept, however, everything started to make much more sense:

A TPC plugin is essentially a state machine.

In this article, I want to explain how I understand TPC plugins now, using a practical scenario: an IDIRA (CyberArk) CPM plugin that launches a PowerShell script to change a user’s password through a REST API.

The goal is not to build the complete plugin here. Instead, I want to focus on the architecture and, especially, on how these components interact:

  • Process.ini
  • Prompts.ini
  • PowerShell
  • stdout
  • API responses
  • TPC transitions
  • END
  • FAIL()
  • CPM error codes

The Scenario

Imagine that we need to manage accounts stored in an application that exposes a REST API.

Our architecture looks roughly like this:

CyberArk CPM
|
v
CyberArk TPC
|
v
PowerShell
|
v
REST API

The PowerShell script performs the actual REST API operations.

TPC, on the other hand, controls the execution of that script.

This distinction is important.

I like to think about the responsibilities this way:

Process.ini
"What should I do?"
Prompts.ini
"What did I receive?"
PowerShell
"Do the actual work."

Once I started thinking about TPC this way, the relationship between the files became much easier to understand.


First: What Is a State Machine?

Before discussing the files, we need to understand the basic idea of a state machine.

The name sounds more complicated than the concept actually is.

A state machine basically says:

“I am currently at this step. If something happens, move to another step.”

Think about a washing machine:

OFF
|
v
FILLING WATER
|
v
WASHING
|
v
RINSING
|
v
SPINNING
|
v
FINISHED

Each step is a state.

The movement from one state to another is a transition.

For example:

Current state:
WASHING
Condition:
Washing time finished
Next state:
RINSING

TPC follows a very similar concept.

A simplified password-change workflow could look like this:

Start PowerShell
|
v
Get Current Password
|
v
Get New Password
|
v
Call API
|
+----------+
| |
Success Error
| |
v v
END FAIL

This is our state machine.


The Role of Process.ini

The Process.ini file defines the workflow of the plugin.

Two particularly important sections are:

[states]

and:

[transitions]

A state represents something TPC can execute.

For example:

[states]
StartScript=(spawn)PowerShell.exe -File OneIdentity.ps1
SendCurrentPassword=<pmpass>
SendNewPassword=<pmnewpass>
END=

The first state starts PowerShell.

The next states can send information to the running process.

Eventually, the plugin reaches either a successful state such as END or an error state.


Starting PowerShell from TPC

One of the things I initially wanted to understand was:

Who actually starts the PowerShell script?

The answer is the Process.ini.

For example:

StartScript=(spawn)PowerShell.exe -File OneIdentity.ps1

The (spawn) built-in action tells TPC to launch the process.

The architecture therefore becomes:

CPM
|
v
TPC
|
| Process.ini
|
| (spawn)
v
PowerShell.exe
|
v
OneIdentity.ps1

This also means that Prompts.ini does not launch PowerShell.

That file has a completely different job.


The Role of Prompts.ini

If Process.ini tells TPC what to do, Prompts.ini helps TPC understand what it received.

Suppose our PowerShell script writes:

Write-Output "CA_REQUEST_CURRENT_PASSWORD"

TPC receives this through the PowerShell output.

We can define a condition in Prompts.ini:

[conditions]
GetCurrentPassword=CA_REQUEST_CURRENT_PASSWORD

We have effectively told TPC:

“If you see CA_REQUEST_CURRENT_PASSWORD, consider the condition GetCurrentPassword to have occurred.”

That condition can now be used by the state machine.


Understanding TPC Transitions

This was probably the part that made everything click for me.

Inside Process.ini, we can have:

[transitions]
StartScript,GetCurrentPassword,SendCurrentPassword

The format is:

CurrentState,Condition,NextState

In our example:

StartScript,GetCurrentPassword,SendCurrentPassword
| | |
| | |
Process.ini Prompts.ini Process.ini

We can read this almost like an English sentence:

“While I am in StartScript, if I receive the GetCurrentPassword condition, move to SendCurrentPassword.”

That is the state machine in action.


How PowerShell and TPC Communicate

This is where TPC becomes particularly interesting.

The PowerShell script and TPC can effectively have a conversation.

PowerShell can write something to stdout:

Write-Output "CA_REQUEST_CURRENT_PASSWORD"

TPC receives it.

Prompts.ini recognizes it:

GetCurrentPassword=CA_REQUEST_CURRENT_PASSWORD

The transition then tells TPC what to do:

StartScript,GetCurrentPassword,SendCurrentPassword

And the next state might be:

SendCurrentPassword=<pmpass>

The communication looks like this:

PowerShell
|
| stdout
|
| CA_REQUEST_CURRENT_PASSWORD
v
TPC
|
| Prompts.ini matches it
v
GetCurrentPassword
|
| transition
v
SendCurrentPassword
|
| <pmpass>
v
PowerShell

PowerShell can read the value using standard input, for example:

$currentPassword = [Console]::ReadLine()

The same concept can be used for the new password.


Why Not Pass Passwords on the PowerShell Command Line?

It might be tempting to do something like:

PowerShell.exe script.ps1 username CurrentPassword NewPassword

But that is not a good design for credentials.

Instead, TPC can start the PowerShell process and then provide sensitive information through the interaction with the running process.

For example:

TPC starts PowerShell
|
v
PowerShell asks for current password
|
v
TPC sends <pmpass>
|
v
PowerShell asks for new password
|
v
TPC sends <pmnewpass>

The PowerShell script doesn’t need to retrieve those passwords directly from the Vault.

TPC already has access to the relevant CPM variables.


Calling the REST API

Once PowerShell has the information it needs, it can perform the real operation.

Conceptually:

try {
Write-Output "DEBUG: Calling password change API"
$response = Invoke-RestMethod `
-Uri $ApiUrl `
-Method POST `
-Body $Body
Write-Output "CA_API_SUCCESS"
}
catch {
Write-Output "CA_API_ERROR"
}

But we can make this much better.

Instead of simply returning a generic error, PowerShell can interpret the API response and translate it into something meaningful to TPC.


Using Control Messages

I found it useful to think of PowerShell output as having two categories.

The first category is control messages.

For example:

CA_API_SUCCESS
CA_AUTHENTICATION_FAILED
CA_USER_NOT_FOUND
CA_PASSWORD_POLICY_REJECTED
CA_SERVER_ERROR
CA_UNEXPECTED_ERROR

These messages are designed specifically for TPC.

They can be mapped in Prompts.ini:

[conditions]
Success=CA_API_SUCCESS
AuthenticationFailed=CA_AUTHENTICATION_FAILED
UserNotFound=CA_USER_NOT_FOUND
PasswordPolicyRejected=CA_PASSWORD_POLICY_REJECTED
ServerError=CA_SERVER_ERROR
UnexpectedError=CA_UNEXPECTED_ERROR

The state machine can then react appropriately.


Don’t Make TPC Understand the Entire API

One design decision I particularly like is keeping API-specific complexity inside PowerShell.

Imagine the API returns:

HTTP 400

with a JSON response indicating:

Password does not meet complexity requirements.

Instead of making Prompts.ini understand the entire JSON response, PowerShell can translate it:

HTTP 400
+
API response
+
PowerShell interpretation
|
v
CA_PASSWORD_POLICY_REJECTED

TPC only needs to understand:

CA_PASSWORD_POLICY_REJECTED

This gives us a nice separation:

REST API
|
| complicated HTTP/JSON response
v
PowerShell
|
| interprets the response
v
Simple CONTROL message
|
v
Prompts.ini
|
v
Process.ini

PowerShell becomes the translation layer between the external API and TPC.


Not Every PowerShell Output Needs to Be in Prompts.ini

This was another important realization for me.

PowerShell can generate output that is useful for troubleshooting without using that output to control the state machine.

For example:

Write-Output "DEBUG: Starting password change"
Write-Output "DEBUG: Target user = $Username"
Write-Output "DEBUG: Calling REST API"
Write-Output "DEBUG: HTTP Status = $StatusCode"
Write-Output "DEBUG: API returned an error"
Write-Output "CA_USER_NOT_FOUND"

Only:

CA_USER_NOT_FOUND

needs to be defined in Prompts.ini.

The other messages exist for troubleshooting.

Conceptually:

PowerShell stdout
|
+--- DEBUG: Calling REST API
| |
| +--> useful for humans
|
+--- DEBUG: HTTP Status = 404
| |
| +--> useful for humans
|
+--- CA_USER_NOT_FOUND
|
+--> Prompts.ini
|
v
State machine

This gives us much better visibility when investigating a plugin failure.


Debug Messages vs Control Messages

I therefore like to keep a clear distinction.

Diagnostic messages:

DEBUG: Authentication request started
DEBUG: Authentication successful
DEBUG: Password change request started
DEBUG: HTTP status = 400
DEBUG: API error message = Password policy violation

Control messages:

CA_API_SUCCESS
CA_AUTHENTICATION_FAILED
CA_USER_NOT_FOUND
CA_PASSWORD_POLICY_REJECTED
CA_SERVER_ERROR
CA_UNEXPECTED_ERROR

Diagnostic messages are primarily for humans.

Control messages are primarily for the TPC state machine.

That distinction makes the plugin much easier to troubleshoot and maintain.


Be Very Careful With Debug Output

There is an important security consideration here.

If PowerShell output can end up in TPC/CPM traces, we should never do this:

Write-Output "Current password = $CurrentPassword"
Write-Output "New password = $NewPassword"
Write-Output "Bearer token = $Token"

The same applies to blindly logging complete REST request bodies:

Write-Output ($Body | ConvertTo-Json)

If the body contains the new password, we have just written a credential into a log file.

Instead, log the operation:

Write-Output "DEBUG: Current password received from TPC"
Write-Output "DEBUG: New password received from TPC"
Write-Output "DEBUG: Authentication token obtained"
Write-Output "DEBUG: Password change request submitted"
Write-Output "DEBUG: HTTP Status = $StatusCode"

Log what happened, not the secrets used to make it happen.


What Does FAIL() Do?

Another important part of Process.ini is the FAIL() built-in action.

For example:

UserNotFound=FAIL(One Identity user was not found,8102)

When the state machine reaches UserNotFound, TPC terminates the plugin execution as a failure.

We can think about the syntax as:

FAIL(ErrorMessage,ErrorCode)

For example:

One Identity user was not found

is the human-readable error, while:

8102

is the plugin return code.

A transition could look like:

CallAPI,UserNotFoundCondition,UserNotFound

The complete flow becomes:

PowerShell
|
| CA_USER_NOT_FOUND
v
Prompts.ini
|
| UserNotFoundCondition
v
Process.ini transition
|
v
UserNotFound
|
v
FAIL(One Identity user was not found,8102)
|
v
TPC terminates with failure
|
v
CPM

END vs FAIL()

This also makes the difference between END and FAIL() easy to understand.

Successful path:

API
|
| success
v
PowerShell
|
| CA_API_SUCCESS
v
Prompts.ini
|
| Success
v
Process.ini
|
v
END
|
v
SUCCESS

Failure path:

API
|
| user doesn't exist
v
PowerShell
|
| CA_USER_NOT_FOUND
v
Prompts.ini
|
| UserNotFound
v
Process.ini
|
v
FAIL(...,8102)
|
v
FAILURE

In other words, most paths through our state machine will eventually lead to either:

END

or:

FAIL()

Designing Good Plugin Error Codes

I also wouldn’t simply make the plugin error code equal to the HTTP status.

For example:

HTTP 401 → plugin error 401
HTTP 404 → plugin error 404

The HTTP status describes what happened at the REST protocol/API level.

The plugin return code should describe the problem from the CPM/plugin perspective.

For example, we might define:

8101 - Authentication failed
8102 - Target user not found
8110 - Authorization denied
8141 - Password rejected by policy
8142 - Password change not permitted
8161 - API timeout
8162 - Connection failure
8163 - TLS/certificate problem
8164 - API server error
8181 - Invalid API response
8199 - Unexpected error

The exact range should be chosen carefully for your environment and checked against CyberArk’s documented/reserved codes, but the important idea is to create a consistent error catalog instead of choosing random numbers.

The architecture then becomes:

HTTP/API response
|
v
PowerShell determines the meaning
|
v
CONTROL MESSAGE
|
v
Prompts.ini condition
|
v
Process.ini transition
|
v
Plugin error code

For example:

HTTP 400
+
"Password does not meet policy"
|
v
CA_PASSWORD_POLICY_REJECTED
|
v
PasswordPolicyRejected
|
v
FAIL(New password rejected by password policy,8141)

This is much more useful than simply reporting:

HTTP 400

to whoever is troubleshooting the CPM.


Putting Everything Together

Let’s look at the complete architecture one more time.

                     CyberArk CPM

|

v

CyberArk TPC

|

Process.ini

|

spawn

|

v

PowerShell

|

"CA_REQUEST_PASSWORD"

|

stdout

|

v

Prompts.ini

|

condition matched

|

v

Process.ini transition

|

Send <pmpass>
>
|

stdin

|

v

PowerShell

|

v

REST API

|

HTTP + JSON

|

v

PowerShell

|

interprets result

|

+-------------+-------------+

| |

CA_API_SUCCESS CA_USER_NOT_FOUND

| |

v v

Prompts.ini Prompts.ini

| |

v v

Process.ini Process.ini

| |

v v

END FAIL(...,8102)

| |

v v

SUCCESS FAILURE

Once I understood this flow, TPC stopped looking like a collection of unrelated INI files and started looking like a relatively simple orchestration engine.


The Mental Model I Use

If I had to summarize everything in a few lines, this is what I would remember:

Process.ini
= What should I DO?
Prompts.ini
= What did I RECEIVE?
PowerShell
= Do the real work.
stdout
= PowerShell talks to TPC.
stdin
= TPC talks to PowerShell.
Conditions
= Give meaning to received output.
Transitions
= Decide where the state machine goes next.
END
= Finish successfully.
FAIL()
= Finish with an error.

And perhaps the most useful sentence of all:

“I am in this state. If I receive this condition, move to that state.”

That simple sentence explains a surprisingly large part of how a CyberArk TPC plugin works.


Final Thoughts

TPC plugins initially looked more complicated to me than they actually were.

The breakthrough was understanding that Process.ini, Prompts.ini, and PowerShell are not competing with each other. They have different responsibilities.

Process.ini orchestrates the workflow.

Prompts.ini recognizes conditions.

PowerShell handles the application-specific logic — in this case, communicating with a REST API.

Once those responsibilities are clearly separated, it becomes much easier to design the plugin, troubleshoot failures, and add meaningful logging without turning the state machine into a collection of API-specific details.

In a future article, I may take this architecture one step further and build a practical TPC plugin example from beginning to end, including the Process.ini, Prompts.ini, PowerShell script, API error handling, and troubleshooting through TPC traces.

Official Reference

CyberArk documentation:

Terminal Plugin Controller (TPC)
CyberArk — Terminal Plugin Controller

Comments

Leave a comment