# Creating a CyberArk (now Idira) Central Policy Manager plugin for an API using WSChains

[In a previous article](https://timschindler.blog/creating-a-cyberark-central-policy-manager-plugin-for-a-web-application), we created a CyberArk (now Idira) Central Policy Manager plugin for the web application Shopizer using the [Web Application CPM Plugin Framework](https://cyberark-customers.force.com/mplace/s/#a352J000000WU3aQAG-a392J0000013WwCQAU) and while it works, we had to employ an awkward workaround to get reconcile functioning. As Shopizer's web interfaces leverage its APIs, we should skip the middleman and have our CPM plugin interface those.

[WSChains is an undocumented, unsupported, used-by-CyberArk-interally framework to build CPM plugins that interact with APIs.](https://timschindler.blog/exploring-cyberark-central-policy-managers-wschains) Using our existing knowledge of Shopizer, examples of WSChains-based plugins from the CyberArk Marketplace, we will build a WSChains-based CPM plugin to manage Shopizer users.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This is article is one in a <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://timschindler.blog/series/shopizer-admin-ext" style="pointer-events: none;">series of articles around creating PSM connectors and CPM plugins</a>.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">❗</div>
<div data-node-type="callout-text">Interested in seeing WSChains becoming a documented and supported option? <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://community.cyberark.com/s/article/Document-and-support-the-WSChains-CPM-plugin-84e7-e8d" style="pointer-events: none;">Vote on this enhancement request</a>!</div>
</div>

# Developing the WSChains plugin

Armed with [Shopizer REST API documentation](https://app.swaggerhub.com/apis/shopizer/shopizer-rest-api) and a bit of knowledge about WSChains, we begin. We will re-use the Shopizer administrator users [we previously created](https://timschindler.blog/creating-a-cyberark-central-policy-manager-plugin-for-a-web-application#heading-setting-the-stage).

## Creating a new web application platform

We can clone the [web application framework-based platform](https://timschindler.blog/creating-a-cyberark-central-policy-manager-plugin-for-a-web-application#heading-creating-a-new-web-application-platform) and make some changes:

*   Under Automatic Password Management -> CPM Plug-in, change `DLLName` to `Cyberark.Extensions.Plugin.WSChains.dll`.
    
*   Under Automatic Password Management -> Generate Password, update `PasswordForbiddenChars` to include `<>&\"'`, which are characters that need to be escaped inside of XML documents.
    
*   Under Automatic Password Management -> Additional Policy Settings, set `Port` to `8080` as this is the default port the Shopizer REST API listens on. `Debug` can be set to `Yes` but a majority of testing will be done outside of CyberArk.
    
*   Under Automatic Password Management -> Additional Policy Settings -> Parameters, we only need a single, additional parameter named `ChainsFilePath` with the value of `bin\ShopizerAdministratorChains.xml` -- the relative path to the XML `Chains` file that holds the operations of the CPM plugin.
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1722603063695/d3d2a04e-8f7e-4e68-8987-942e8f14fd3e.png align="center")

<div data-node-type="callout">
<div data-node-type="callout-emoji">❗</div>
<div data-node-type="callout-text">The WSChains DLL <code>Cyberark.Extensions.Plugin.WSChains.dll</code> is not available as part of the CPM's base installation. You need to acquire the DLL as part of a platform package from the CyberArk Marketplace or another source.</div>
</div>

## Preparing our parameters file for testing

We can test a WSChains plugin the same way we can test any other and we will do this to facilitate faster and easier development of our plugin. The [Parameters file](https://docs.cyberark.com/pam-self-hosted/latest/en/Content/PASIMP/Plug-in-NetInvoker_Test.htm?tocpath=Developer%7CCreate%20extensions%7CCreate%20CPM%20plugins%7CCredentials%20Management%20.NET%20SDK%7C_____5#Parameterfile) looks like the following:

```ini
[targetaccount]
username=shopizeradministrator@timschindler.dev
newpassword=Password2
password=Password2
safename=Safename
foldername=Foldername
objectname=Objectname
PolicyID=PolicyID
; The machine where the Shopizer instance is running.
Address=192.168.178.61
; The port the Shopizer REST API is running on.
Port=8080

[extrapass3]
username=reconcile@timschindler.dev
password=Password1

[extrainfo]
; Path to the chains file.
ChainsFilePath=bin\ShopizerAdministratorChains.xml
Debug=Yes
```

This is saved as an `.ini` file in the root of the CPM folder.

# Crafting `ShopizerAdministratorChains.xml`

Like a `WebFormFieldsFile`, the Chains file will hold the details for all three operations of our CPM plugin. For each operation, we will need to define the necessary requests that will be used by the links in the chains that represent each CPM operation.

We start with the bare minimum and some Fails.

```xml
<WSChains name="ShopizerAdministrator">
	<GlobalSettings>
	</GlobalSettings>
	<Requests>
	</Requests>
	<Fails>
		<Fail name="FAILBadRequest" rc="8101" message="Status Code = 400. Bad Request" />
		<Fail name="FAILUnauthorized" rc="8102" message="Status Code = 401. The request sent by the CPM could not be authenticated" />
		<Fail name="FAILForbidden" rc="8103" message="Status Code = 403. Access to the requested (valid) URL by the client is forbidden" />
		<Fail name="FAILNotFound" rc="8104" message="Status Code = 404. Resource not found." />
		<Fail name="FAILMethodNotFound" rc="8105" message="Status Code = 405. Method not allowed." />
		<Fail name="FAILRequestConflict" rc="8106" message="Status Code = 409. Request conflict with current state of the server" />
		<Fail name="FAILInternalServerError" rc="8107" message="Status Code = 500. The server cannot process the request for an unknown reason" />
		<Fail name="FAILWebServer" rc="8108" message="Status Code = 501. The server cannot process the request for an unknown reason" />
		<Fail name="FAILGeneral" rc="8109" message="General Error. Check the logs for more information." />
		<Fail name="FAILUnauthorizedCurrPass" rc="8110" message= "Failed to authenticate to the API using the target account username and current password. Status code 401." />
		<Fail name="FAILUnauthorizedNewPass" rc="8111" message= "Failed to authenticate to the API using the target account username and new password. Status code 401." />
		<Fail name="FAILToParseRecUsername" rc="8112" message= "Failed to parse the target account ID to perform a reconciliation" />
	</Fails>

	<Chains>
	</Chains>
</WSChains>
```

Save the file as `ShopizerAdministratorChains.xml` in the `bin` folder of the CPM.

## Verify

Like the web application framework plugin, we will simply authenticate to the Shopizer REST API. Authenticating with a correct username and password will result in an authentication token.

In the `Requests` element, the following `RequestWrapper` needs to be added based on the [Shopizer REST API documentation](https://app.swaggerhub.com/apis/shopizer/shopizer-rest-api/3.0.1#/User%20authentication%20Api/authenticateUsingPOST_1):

```xml
<Requests>
	<RequestWrapper>
		<request name="GetToken">
			<version>2</version>
			<general>
				<endpoint>/api/v1/private/login</endpoint>
				<method>POST</method>
			</general>
			<header>
				<Content-Type>application/json</Content-Type>
				<Accept>application/json</Accept>
				<User-Agent>Integration/1.0 (CyberArk, CPM, 1)</User-Agent>
			</header>
			<body>
				<json>
					{
						"username":"{{username}}",
						"password":"{{pmpass}}"
					}
				</json>
			</body>
		</request>
	</RequestWrapper>
</Requests>
```

Inside the `request` element, the `name` attribute is used to refer to the Request in our Chain's links. `{{username}}` and `{{pmpass}}` will be replaced with the username and password of the account when the request is executed.

Under the `Chains` element, we create a `Chain` with the name of `verifypass`. It contains only a single `Link`:

```xml
<Chain name="verifypass">
	<Link name="VerifyPasswordLink" request="GetToken">
		<StatusCode value="200" next="END">
			<Parse name="parsed/token" type="json" value="token" secure="true"/>
		</StatusCode>
		<StatusCode value="400" next="FAILBadRequest"/>
		<StatusCode value="401" next="FAILUnauthorized"/>
		<StatusCode value="403" next="FAILForbidden"/>
		<StatusCode value="404" next="FAILNotFound"/>
		<StatusCode value="405" next="FAILMethodNotFound"/>
		<StatusCode value="409" next="FAILRequestConflict"/>
		<StatusCode value="500" next="FAILInternalServerError"/>
		<StatusCode value="501" next="FAILWebServer"/>
		<StatusCode value="*" next="FAILGeneral"/>
	</Link>
</Chain>
```

The `Link` object is mostly self-explanatory: the `request` attribute defines what `Request` is invoked and the different `StatusCode` elements determine how the response to the request is handled.

<div data-node-type="callout">
<div data-node-type="callout-emoji">❗</div>
<div data-node-type="callout-text">There is not a hard requirement to having a child <code>Parse</code> element under the <code>StatusCode</code> that represents the end of the CPM operation however not parsing the returned authentication token will result in it being shown in plaintext in the debug logs!</div>
</div>

When testing, we quickly get positive feedback:

```powershell
PS C:\Program Files (x86)\CyberArk\Password Manager> .\bin\CANetPluginInvoker.exe shopizerwschains.ini verifypass Cyberark.Extensions.Plugin.WSChains.dll True
The plugin ended successfully (RC = 0)
```

Looking in the debug logs, we get a nice picture of what the plugin is doing.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1722607018599/5414fb2c-406a-4725-b372-48d2531c51a9.png align="center")

## Change

Change is made simple for us as in the same response that provides the authentication token, it provides the `id` of the user the token is for. This is important [as we need to specify the `id` of the user whose password we are changing as part of the request URL](https://app.swaggerhub.com/apis/shopizer/shopizer-rest-api/3.0.1#/User%20management%20resource%20\(User%20Management%20Api\)/passwordUsingPATCH).

As part of the change operation, at the end, we are going to authenticate with the new password to be sure of the change so we need three `requests` in total. We can re-use the `GetToken` request but we need one represent the change operation itself as well as logging in with the new password:

```xml
<Requests>
	<RequestWrapper>
		<request name="ChangePassword">
			<version>2</version>
			<general>
				<endpoint>/api/v1/private/user/{{parsed/id}}/password</endpoint>
				<method>PATCH</method>
			</general>
			<header>
				<Content-Type>application/json</Content-Type>
				<Accept>application/json</Accept>
				<User-Agent>Integration/1.0 (CyberArk, CPM, 1)</User-Agent>
				<Authorization>Bearer {{parsed/token}}</Authorization>
			</header>
			<body>
				<json>
				  {
				  "password": "{{pmpass}}",
				  "changePassword": "{{pmnewpass}}"
				  }
				</json>
			</body>
		</request>
	</RequestWrapper>
	<RequestWrapper>
		<request name="GetTokenWithNewPass">
			<version>2</version>
			<general>
				<endpoint>/api/v1/private/login</endpoint>
				<method>POST</method>
			</general>
			<header>
				<Content-Type>application/json</Content-Type>
				<Accept>application/json</Accept>
				<User-Agent>Integration/1.0 (CyberArk, CPM, 1)</User-Agent>
			</header>
			<body>
				<json>
					{
						"username":"{{username}}",
						"password":"{{pmnewpass}}"
					}
				</json>
			</body>
		</request>
	</RequestWrapper>
</Requests>
```

We add the `Authorization` header with the Bearer token we received from the `GetToken` request to the `ChangePassword` request. `GetTokenWithNewPass` looks exactly like `GetToken` but we use `{{pmnewpass}}` as the password.

Under the `Chains` element, we create a `Chain` with the name of `changepass` with three `Links`:

```xml
<Chains>
	<Chain name="changepass">
		<Link name="ChangePasswordGetTokenLink" request="GetToken">
			<StatusCode value="200" next="ChangePasswordLink">
				<Parse name="parsed/token" type="json" value="token" secure="true"/>
				<Parse name="parsed/id" type="json" value="id" secure="false"/>
			</StatusCode>
			<StatusCode value="400" next="FAILBadRequest"/>
			<StatusCode value="401" next="FAILUnauthorizedCurrPass"/>
			<StatusCode value="403" next="FAILForbidden"/>
			<StatusCode value="404" next="FAILNotFound"/>
			<StatusCode value="405" next="FAILMethodNotFound"/>
			<StatusCode value="409" next="FAILRequestConflict"/>
			<StatusCode value="500" next="FAILInternalServerError"/>
			<StatusCode value="501" next="FAILWebServer"/>
			<StatusCode value="*" next="FAILGeneral"/>
		</Link>
		<Link name="ChangePasswordLink" request="ChangePassword">
			<StatusCode value="200" next="VerifyNewPassLink"/>
			<StatusCode value="400" next="FAILBadRequest"/>
			<StatusCode value="401" next="FAILUnauthorized"/>
			<StatusCode value="403" next="FAILForbidden"/>
			<StatusCode value="404" next="FAILNotFound"/>
			<StatusCode value="405" next="FAILMethodNotFound"/>
			<StatusCode value="409" next="FAILRequestConflict"/>
			<StatusCode value="500" next="FAILInternalServerError"/>
			<StatusCode value="501" next="FAILWebServer"/>
			<StatusCode value="*" next="FAILGeneral"/>
		</Link>
		<Link name="VerifyNewPassLink" request="GetTokenWithNewPass">
			<StatusCode value="200" next="END">
				<Parse name="parsed/tokennew" type="json" value="token" secure="true" />
			</StatusCode>
			<StatusCode value="400" next="FAILBadRequest"/>
			<StatusCode value="401" next="FAILUnauthorizedNewPass"/>
			<StatusCode value="403" next="FAILForbidden"/>
			<StatusCode value="404" next="FAILNotFound"/>
			<StatusCode value="405" next="FAILMethodNotFound"/>
			<StatusCode value="409" next="FAILRequestConflict"/>
			<StatusCode value="500" next="FAILInternalServerError"/>
			<StatusCode value="501" next="FAILWebServer"/>
			<StatusCode value="*" next="FAILGeneral"/>
		</Link>
	</Chain>
</Chains>
```

The first `Link` is copied from our verify operation's `Chain`. The only difference is the addition of the `Parse` element that captures the `id` of the account so we can use it in the next Link. The `next` attribute of the Link tells the plugin the name of the Link in the Chain to execute.

`ChangePasswordLink` invokes the `ChangePassword` request and when executed successfully (the response's status code is `200`), `VerifyNewPassLink` is called to login with the password just set. Again we parse the result so that we can mask the authentication token from the response in the logs.

Testing shows success:

```powershell
PS C:\Program Files (x86)\CyberArk\Password Manager> .\bin\CANetPluginInvoker.exe shopizerwschains.ini changepass Cyberark.Extensions.Plugin.WSChains.dll True
The plugin ended successfully (RC = 0)
```

## Reconcile

Reconcile is complex. We need to use [a different endpoint](https://app.swaggerhub.com/apis/shopizer/shopizer-rest-api/3.0.1#/User%20management%20resource%20\(User%20Management%20Api\)/updateUsingPUT_16) when changing the password of another user. The request URL includes the `id` of the user to be changed -- requiring us to either store it as an account property or look this up as part of the CPM operation -- and because it is a PUT operation, we need to provide all the existing details of the user along with the new password, otherwise they will be deleted from the user's profile.

There is [an endpoint that will list all users and their `id`](https://app.swaggerhub.com/apis/shopizer/shopizer-rest-api/3.0.1#/User%20management%20resource%20\(User%20Management%20Api\)/listUsingGET_18) so we will need to call this as a link in our reconcile operation's chain and parse the response. We will then [retrieve the user's profile](https://app.swaggerhub.com/apis/shopizer/shopizer-rest-api/3.0.1#/User%20management%20resource%20\(User%20Management%20Api\)/getUsingGET_14) in order to get the information we need to pass along as part of the change password request.

We need the following, additional requests:

```xml
<Requests>
	<RequestWrapper>
		<request name="RecGetToken">
			<version>2</version>
			<general>
				<endpoint>/api/v1/private/login</endpoint>
				<method>POST</method>
			</general>
			<header>
				<Content-Type>application/json</Content-Type>
				<Accept>application/json</Accept>
				<User-Agent>Integration/1.0 (CyberArk, CPM, 1)</User-Agent>
			</header>
			<body>
				<json>
					{
						"username":"{{extrapass3\username}}",
						"password":"{{pmextrapass3}}"
					}
				</json>
			</body>
		</request>
	</RequestWrapper>
	<RequestWrapper>
		<request name="GetUserByID">
			<version>2</version>
			<general>
				<endpoint>/api/v1/private/users</endpoint>
				<method>GET</method>
			</general>
			<header>
				<Content-Type>application/json</Content-Type>
				<Accept>application/json</Accept>
				<User-Agent>Integration/1.0 (CyberArk, CPM, 1)</User-Agent>
				<Authorization>Bearer {{parsed/token}}</Authorization>
			</header>
			<body>
				<json></json>
			</body>
		</request>
	</RequestWrapper>
	<RequestWrapper>
		<request name="GetUserProfileByID">
			<version>2</version>
			<general>
				<endpoint>/api/v1/private/users/{{parsed/id}}</endpoint>
				<method>GET</method>
			</general>
			<header>
				<Content-Type>application/json</Content-Type>
				<Accept>application/json</Accept>
				<User-Agent>Integration/1.0 (CyberArk, CPM, 1)</User-Agent>
				<Authorization>Bearer {{parsed/token}}</Authorization>
			</header>
			<body>
				<json></json>
			</body>
		</request>
	</RequestWrapper>
	<RequestWrapper>
		<request name="RecChangePassword">
			<version>2</version>
			<general>
				<endpoint>/api/v1/private/user/{{parsed/id}}</endpoint>
				<method>PUT</method>
			</general>
			<header>
				<Content-Type>application/json</Content-Type>
				<Accept>application/json</Accept>
				<User-Agent>Integration/1.0 (CyberArk, CPM, 1)</User-Agent>
				<Authorization>Bearer {{parsed/token}}</Authorization>
			</header>
			<body>
				<json>
				{	
					<!-- No quotes because it needs to be treated as a bool -->
					"active": {{parsed/active}},
					"defaultLanguage": "{{parsed/defaultLanguage}}",
					"firstName": "{{parsed/firstName}}",			
					<!-- The value for the groups key will be a string representation of the groups array. -->
					"groups:": {{parsed/groups}},
					"id": "{{parsed/id}}",
					"lastName": "{{parsed/lastName}}",
					"password": "{{pmnewpass}}",
					"repeatPassword": "{{pmnewpass}}",
					"store": "{{parsed/merchant}}",
					"emailAddress": "{{parsed/emailAddress}}",
					"userName": "{{parsed/userName}}"
				}			
				</json>
			</body>
		</request>
	</RequestWrapper>
</Requests>
```

The `RecGetToken` request is like `GetToken` but uses the linked reconcile account's username and password. The `GetUserByID` and `GetUserProfileByID` requests facilitate retrieving the needed details from the user's profile while `RecChangePassword` is used to change the password, including the parsed details.

The `reconcilepass` Chain will have five links:

```xml
<Chains>
	<Chain name="reconcilepass">
		<Link name="ReconcilePasswordGetTokenLink" request="RecGetToken">
			<StatusCode value="200" next="GetUserByIDLink">
				<Parse name="parsed/token" type="json" value="token" secure="true" />
			</StatusCode>
			<StatusCode value="400" next="FAILBadRequest"/>
			<StatusCode value="401" next="FAILUnauthorizedCurrPass"/>
			<StatusCode value="403" next="FAILForbidden"/>
			<StatusCode value="404" next="FAILNotFound"/>
			<StatusCode value="405" next="FAILMethodNotFound"/>
			<StatusCode value="409" next="FAILRequestConflict"/>
			<StatusCode value="500" next="FAILInternalServerError"/>
			<StatusCode value="501" next="FAILWebServer"/>
			<StatusCode value="*" next="FAILGeneral"/>
		</Link>
		<Link name="GetUserByIDLink" request="GetUserByID">
			<StatusCode value="200" next="ByParse" condition="OR" nomatch="FAILToParseRecUsername">
				<Parse name="parsed/id" type="text" value="&quot;id&quot;:(\d+)(?=[^}]*&quot;userName&quot;:&quot;{{username}}&quot;)">
					<Equals value="Success" next="END"/>
					<Equals value="*" next="GetUserProfileByIDLink"/>
				</Parse>
			</StatusCode>
			<StatusCode value="400" next="FAILBadRequest"/>
			<StatusCode value="401" next="FAILUnauthorized"/>
			<StatusCode value="403" next="FAILForbidden"/>
			<StatusCode value="404" next="FAILNotFound"/>
			<StatusCode value="405" next="FAILMethodNotFound"/>
			<StatusCode value="409" next="FAILRequestConflict"/>
			<StatusCode value="500" next="FAILInternalServerError"/>
			<StatusCode value="501" next="FAILWebServer"/>
			<StatusCode value="*" next="FAILGeneral"/>
		</Link>
		<Link name="GetUserProfileByIDLink" request="GetUserProfileByID">
			<StatusCode value="200" next="ReconcilePasswordLink">
				<!-- Uses method=lowercase as we later treat it as a bool. Works with at least v12.5.7.3 of WSChains.dll-->
				<Parse name="parsed/active" type="json" value="active" method="lowercase"/>
				<Parse name="parsed/defaultLanguage" type="json" value="defaultLanguage" />
				<Parse name="parsed/emailAddress" type="json" value="emailAddress" />
				<Parse name="parsed/firstName" type="json" value="firstName" />
				<!-- Use regex to get the value of the groups key as when we parse it as type json, it cannot convert an Array to string. -->
				<Parse name="parsed/groups" type="text" value="&quot;groups&quot;:(\[[^\]]+\])" />
				<Parse name="parsed/lastName" type="json" value="lastName" />
				<Parse name="parsed/merchant" type="json" value="merchant" />
				<Parse name="parsed/username" type="json" value="userName" />
			</StatusCode>
			<StatusCode value="400" next="FAILBadRequest"/>
			<StatusCode value="401" next="FAILUnauthorized"/>
			<StatusCode value="403" next="FAILForbidden"/>
			<StatusCode value="404" next="FAILNotFound"/>
			<StatusCode value="405" next="FAILMethodNotFound"/>
			<StatusCode value="409" next="FAILRequestConflict"/>
			<StatusCode value="500" next="FAILInternalServerError"/>
			<StatusCode value="501" next="FAILWebServer"/>
			<StatusCode value="*" next="FAILGeneral"/>
		</Link>
		<Link name="ReconcilePasswordLink" request="RecChangePassword">
			<StatusCode value="200" next="RecVerifyNewPassLink"/>
			<StatusCode value="400" next="FAILBadRequest"/>
			<StatusCode value="401" next="FAILUnauthorized"/>
			<StatusCode value="403" next="FAILForbidden"/>
			<StatusCode value="404" next="FAILNotFound"/>
			<StatusCode value="405" next="FAILMethodNotFound"/>
			<StatusCode value="409" next="FAILRequestConflict"/>
			<StatusCode value="500" next="FAILInternalServerError"/>
			<StatusCode value="501" next="FAILWebServer"/>
			<StatusCode value="*" next="FAILGeneral"/>
		</Link>
		<Link name="RecVerifyNewPassLink" request="GetTokenWithNewPass">
			<StatusCode value="200" next="END">
				<Parse name="parsed/tokennew" type="json" value="token" secure="true"/>
			</StatusCode>
			<StatusCode value="400" next="FAILBadRequest"/>
			<StatusCode value="401" next="FAILUnauthorizedNewPass"/>
			<StatusCode value="403" next="FAILForbidden"/>
			<StatusCode value="404" next="FAILNotFound"/>
			<StatusCode value="405" next="FAILMethodNotFound"/>
			<StatusCode value="409" next="FAILRequestConflict"/>
			<StatusCode value="500" next="FAILInternalServerError"/>
			<StatusCode value="501" next="FAILWebServer"/>
			<StatusCode value="*" next="FAILGeneral"/>
		</Link>
	</Chain>
</Chains>
```

The heavy lifting is done in `GetUserByIDLink` and `GetUserProfileByIDLink`.

Regular expression is used to get the `id` of the user we are trying to reconcile in `GetUserByIDLink`. In `GetUserProfileByIDLink`, we can easily parse all the details except for the groups as this is a JSON array and again we use regular expression to grab the array as a string.

Again, our testing validates the chain.

```powershell
PS C:\Program Files (x86)\CyberArk\Password Manager> .\bin\CANetPluginInvoker.exe shopizerwschains.ini reconcilepass Cyberark.Extensions.Plugin.WSChains.dll True
The plugin ended successfully (RC = 0)
```

# Conclusion

WSChains is a great framework and our CPM plugin uses only a small amount of its features. I previously [said it being unsupported by CyberArk makes it unsuitable for production use](https://timschindler.blog/exploring-cyberark-central-policy-managers-wschains#heading-wrap-up) but I no longer agree with that statement. The maturity of WSChains and the functionality of it makes it a no-brainer for developing CPM plugins that interact with REST APIs.

The platform files, Chain file, and parameter file can be found in my [GitHub repository](https://github.com/aaearon/cyberark-shopizer-admin-extensions). If you are interested in developing your own CPM plugin for Shopizer administrator users, you can use [this docker-compose.ymlto quickly stand up a c](https://github.com/aaearon/cyberark-shopizer-admin-extensions/blob/main/shopizer/docker-compose.yml)ontainerized Shopizer environment. Check out the [README](https://github.com/aaearon/cyberark-shopizer-admin-extensions/tree/main#standing-up-your-own-shopizer-environment-with-docker-compose) for more information. The same GitHub repository contains the platform files, Chain file, and a WSChains DLL.
