Skip to main content

Problem Statement:

ZTNA requires that all application objects are uniquely and explicitly defined. In large organisations, IT Security teams often do not have insight into all the applications used by business users and neither do they understand which users should have access to these applications.


Use Case:

Leverage robotic process automation to enable business led application onboarding. Provide a form to business users enabling those users to request a new application. After an approval flow, API’s are then leveraged to provision the application objects in Netskope with the relevant access granted.
 

Proof of concept:

The idea is that any business process automation tool like, Microsoft Power Automate, BMC Helix, tine etc. can be used to assist companies to achieve this use case. I leveraged the community version of n8n to build a simple proof of concept and provide insight into how this type of process can be used to simplify the implementation of a zero trust network access strategy.

Demo of the concept:

 

Requirements:

  • Rest APIv2 Token with r/w access to the following endpoints
    • /api/v2/infrastructure/publishers
    • /api/v2/steering/apps/private
  • Create relevant application tags and add them as input options on the user form.
  • Create real-time policies to map user groups to application tags.
  • Use a naming convention of publishers which will allow you to programatically filter. In my example I use dc1 and dc2 in names as an indicator for location, then have this as options in the form so the user can select app to be published via specific publisher group.
  • Leverage a 3rd party workflow automation tool to build the user form and automation workflow.

Example Private Application Tags available create private app interface on Netskope.

Private Application Tags in Netskope Tenant

 

These tags are exposed to business user as options they can choose through a from. As highlighted in the Add Group Section of the screenshot below.

Example form from n8n

 

In this proof of concept I created a number of real-time policies to allow specific user groups access to specific application tags. This means if a new application is created and a tag applied users will automatically gain access to those applications based on their user group membership.

Example User Groups to App Tag ZTNA Policies

 

Next the form offers the user to select the application location, the idea here is to create and name publisher groups based on location. Then this will allow the user to specify which publisher group they want the application to be published through.

The location field is used to filter publishers that contain the selected value in their name.

 

Essentially this is used in the workflow to filter publishers to the selected data centre, here is the list of publishers in my setup. When the workflow runs it filters the publishers selected by the Location form input before creating and assigning the app to the list of filtered publishers.

Multiple publishers configured across two data centre locations

 

Here is my sample n8n workflow, n8n is a complete topic on its own. Luckily there are really good documentation and a great user forum. In this example, I used email with SMTP but it will be trivial to replace the email notification with a web hook to Microsoft Teams, Slack, Discord etc. Additionally this workflow will not be production friendly as it lacks some key user authentication components, however this aims to inspire so feel free to make it your own.

Snapshot of the workflow created in n8n

I can think of a number of other useful scenarios, for instance

  • Provide a list of all applications accessible by a specific user and associated groups
  • Offer a user the ability to request access from a list of already created applications (self-service)
  • Provide a list of existing applications not accessible
  • Provide user basic self diagnostics, why can I not access x application?
  • Etc.


I would love to see how you have used Netskope API’s to automate business processes please share your approaches?


Paul Beyleveld

Sr. Solutions Engineer

 

Below please find attached the contents of the workflow .json file. Save the code block contents to a text file with .json extensions and import the .json into the n8n workflow editor as a starting point. Be sure to configure the various components that is specific to your environment as per the notes.

{
"name": "Create Private App after Approved Template",
"nodes": [
{
"parameters": {
"path": "1e647600-6ec8-4744-8131-f72f587ecd19",
"formTitle": "Application Request",
"formDescription": "Request new application",
"formFields": {
"values": [
{
"fieldLabel": "Name",
"requiredField": true
},
{
"fieldLabel": "Host (FQDN, IP entries comma separated)",
"fieldType": "textarea",
"requiredField": true
},
{
"fieldLabel": "TCP Ports"
},
{
"fieldLabel": "UDP Ports"
},
{
"fieldLabel": "App Group",
"fieldType": "dropdown",
"fieldOptions": {
"values": [
{
"option": "Developer"
},
{
"option": "SecOps"
},
{
"option": "WindowsServers"
},
{
"option": "Contractors"
}
]
},
"multiselect": true,
"requiredField": true
},
{
"fieldLabel": "Location",
"fieldType": "dropdown",
"fieldOptions": {
"values": [
{
"option": "DC1"
},
{
"option": "DC2"
}
]
},
"multiselect": true,
"requiredField": true
}
]
},
"options": {
"respondWithOptions": {
"values": {
"formSubmittedText": "Your response ZTNA application request has been recorded"
}
}
}
},
"id": "76bbd64e-f643-40e9-ad9b-878f7a478da6",
"name": "n8n Form Trigger",
"type": "n8n-nodes-base.formTrigger",
"typeVersion": 2,
"position": [
500,
-420
],
"webhookId": "eb15d947-1d34-493b-91f6-c88a2adc9882"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "e97aede8-70de-4214-8545-c3a114f53c3c",
"name": "app_name",
"value": "={{ $json.Name }}",
"type": "string"
},
{
"id": "ec343fe5-0262-42c3-ad74-00e740d9e69d",
"name": "host",
"value": "={{ $json['Host (FQDN, IP entries comma separated)'] }}",
"type": "string"
},
{
"id": "22686977-cb0b-452e-8a97-1e32007f8518",
"name": "protocols.tcp",
"value": "={{ $json['TCP Ports'] }}",
"type": "string"
},
{
"id": "3f016d6e-5c7a-45ac-b5da-5331c4b62137",
"name": "protocols.udp",
"value": "={{ $json['UDP Ports'] }}",
"type": "string"
},
{
"id": "f83e07b4-e4d8-4e09-9284-42a3b424ff24",
"name": "tags",
"value": "={{ $json[\"App Group\"] }}",
"type": "string"
},
{
"id": "eaa2aefc-3e87-4455-ac05-7609660684a9",
"name": "location",
"value": "={{ $json.Location }}",
"type": "string"
}
]
},
"options": {}
},
"id": "4d3f55a3-bbb8-423c-82b3-625f04517f64",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.3,
"position": [
700,
-420
],
"notesInFlow": true,
"notes": "Transform captured to required json structure"
},
{
"parameters": {
"method": "POST",
"url": "=https://<tenant>.goskope.com/api/v2/steering/apps/private",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"app_name\": {{ JSON.stringify($('Normalise JSON').item.json[\"app_name\"]) }},\n \"host\": {{ JSON.stringify($('Normalise JSON').item.json[\"host\"]) }},\n \"protocols\": {{ JSON.stringify($('Normalise JSON').item.json[\"protocols\"]) }},\n \"publishers\": {{ JSON.stringify($('Normalise JSON').item.json[\"publishers\"]) }},\n \"tags\": {{ JSON.stringify($('Normalise JSON').item.json[\"tags\"]) }},\n \"use_publisher_dns\": \"false\",\n \"clientless_access\": \"false\",\n \"trust_self_signed_certs\": \"true\"\n} ",
"options": {}
},
"id": "3b625a9c-862e-442a-a7d7-5fd02b963e49",
"name": "Create Private App in Netskope",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
1100,
-100
],
"credentials": {
"httpHeaderAuth": {
"id": "CWgdPiy7Ie4UoGOS",
"name": "Netskope Restv2 Token"
}
}
},
{
"parameters": {
"url": "=https://<tenant>.goskope.com/api/v2/infrastructure/publishers ",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {}
},
"id": "7102bdf0-5800-4d9d-a82c-6c5dc14cb84d",
"name": "Get Publishers from Netskope",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
900,
-420
],
"credentials": {
"httpHeaderAuth": {
"id": "CWgdPiy7Ie4UoGOS",
"name": "Netskope Restv2 Token"
}
}
},
{
"parameters": {
"jsCode": "const publishers = [];\n//pubstofilter is created from the user input form\nconst pubstoFilter = JSON.parse($('Edit Fields').item.json.location.toLowerCase());\n\n//console.log(pubstoFilter);\n\nfor(let i=0; i<$input.all().length; i++) {\n const allpublishers = $input.all()[i].json.data.publishers;\n //console.log(allpublishers);\n\n for (let j = 0; j < allpublishers.length; j++) {\n \n const publisher_names = allpublishers[j].publisher_name.trim();\n let matchFound = false;\n \n for (let k = 0; k < pubstoFilter.length; k++) {\n //console.log(publisher_names,pubstoFilter[k]);\n //filter only publishers that contain text capture by the user input form\n if (publisher_names.includes(pubstoFilter[k])) {\n matchFound = true;\n break;\n }\n }\n \n if (matchFound) {\n publishers.push({\n publisher_id: allpublishers[j].publisher_id,\n publisher_name: allpublishers[j].publisher_name.trim()\n });\n }\n \n }\n \n}\n\nreturn [{\"publishers\": publishers }];\n\n"
},
"id": "8182b5d5-c87a-4efa-b460-c358e292309b",
"name": "Extract Publishers",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1100,
-420
]
},
{
"parameters": {
"jsCode": "//JSON.parse($('Edit Fields'));\n\nconst input = $('Edit Fields').all();\nconst publishers = $('Extract Publishers').all();\nconsole.log(publishers)\n\nconst transformed = [];\n\nfor (let i = 0; i < input.length; i++) {\n const item = input[i].json;\n console.log(item);\n const protocols = [];\n \n for (const type in item.protocols) {\n if (item.protocols[type] !== \"\") {\n const ports = item.protocols[type].split(',');\n for (let j = 0; j < ports.length; j++) {\n protocols.push({\n type,\n port: ports[j].trim()\n });\n }\n }\n }\n \n transformed.push({ protocols });\n //console.log(transformed);\n $input.all()[i].json.protocols = transformed[0].protocols;\n\n const tags =[];\n const tag_name = JSON.parse(item.tags);\n\n console.log(tag_name);\n for (let i = 0; i < tag_name.length; i++) {\n //for (const tag in tagname) {\n //console.log(tag[0])\n tags.push({\n tag_name: tag_name[i].trim()\n });\n\n }\n\n $input.all()[i].json.app_name = input[i].json.app_name\n $input.all()[i].json.host = input[i].json.host\n $input.all()[i].json.publishers = publishers[0].json.publishers;\n $input.all()[i].json.tags = tags;\n\n}\n\n\nreturn $input.all();"
},
"id": "398018d2-8087-4e83-9c98-ff01dfdc3e21",
"name": "Normalise JSON",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1300,
-420
]
},
{
"parameters": {
"fromEmail": "=DevSecOps <noreply@example.com>",
"toEmail": "=ztna-approvals@example.com",
"subject": "ZTNA App Request",
"html": "=<p>New ZTNA Application Request</p>\n\n<p>\n<ul>\n <li>Application: {{ $json.app_name }}</li>\n <li>Hosts: {{ $json.host }}</li>\n <li>TCP Ports: {{ $('Edit Fields').item.json.protocols.tcp }}</li>\n <li>UDP Ports: {{ $('Edit Fields').item.json.protocols.udp }}</li>\n <li>App Group Tag: {{ $('Edit Fields').item.json.tags }}</li>\n</ul>\n</p>\n\n\n<p>Please approve or decline new application request by clicking <a href=\"{{ $resumeWebhookUrl }}?action=approve\">Approve</a> or <a href=\"{{ $resumeWebhookUrl }}?action=decline\">Decline</a></p> ",
"options": {}
},
"id": "2f7b4b60-3218-4b72-a7d4-bce9f92d22ed",
"name": "Request Approval",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [
500,
20
],
"alwaysOutputData": false,
"credentials": {
"smtp": {
"id": "vwuPdKj1qOvXyKi6",
"name": "SMTP account"
}
}
},
{
"parameters": {
"fromEmail": "=DevSecOps <paul@beyleveld.org>",
"toEmail": "=pbeyleveld@netskope.com",
"subject": "ZTNA App Created",
"html": "=<p>New ZTNA Application Request</p>\n<p>\nApplication: {{ $('Normalise JSON').item.json.app_name }} has been successfully created on Netskope.\n</p>\n\n",
"options": {}
},
"id": "f9644134-0138-4e26-ac31-17c11b6426cc",
"name": "Notify Success",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [
1300,
-100
],
"alwaysOutputData": false,
"credentials": {
"smtp": {
"id": "vwuPdKj1qOvXyKi6",
"name": "SMTP account"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "4977724e-bbc9-4f12-a056-0429fa139083",
"leftValue": "={{ $json.query.action }}",
"rightValue": "approve",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "e09e392b-ee2f-4266-9745-e62b61e478e5",
"name": "Check Response",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
900,
20
]
},
{
"parameters": {
"resume": "webhook",
"options": {
"responseData": "=<!DOCTYPE html>\n<html>\n<body>\n\n<h1>Response Captured</h1>\n\n</body>\n</html>"
}
},
"id": "95b8bb04-326a-43e9-9593-fa95123cb208",
"name": "Approval Wait",
"type": "n8n-nodes-base.wait",
"typeVersion": 1,
"position": [
700,
20
],
"webhookId": "85e4793a-f44a-43e6-b464-4948a447c4ef"
},
{
"parameters": {
"fromEmail": "=DevSecOps <paul@beyleveld.org>",
"toEmail": "=pbeyleveld@netskope.com",
"subject": "ZTNA App Request Declined",
"html": "=<p>New ZTNA Application Request</p>\n<p>\nApplication: {{ $('Normalise JSON').item.json.app_name }} request has been declined.\n</p>\n\n",
"options": {}
},
"id": "9acd6453-2205-423e-bf58-1ea154587850",
"name": "Notify Decline",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [
1100,
140
],
"alwaysOutputData": false,
"credentials": {
"smtp": {
"id": "vwuPdKj1qOvXyKi6",
"name": "SMTP account"
}
}
},
{
"parameters": {
"content": "Replace **<tenant>** with you Netskope tenant name and set the RESTv2 token as **Netskope-Api-Token** header",
"height": 306.6594943518019,
"width": 191.45777299623444
},
"id": "0a24fed7-012c-41a4-a49a-a3553155ac95",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
860,
-540
]
},
{
"parameters": {
"content": "Replace the **<tenant>** placeholder with your tenant name.",
"height": 261.2157073695536,
"width": 188.35933297471755
},
"id": "efaccf41-6001-47b7-b37a-248b89531c86",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1060,
-180
]
},
{
"parameters": {
"content": "Replace the **From Mail** and **To Mail** fields and ensure you have a working SMTP configuration in n8n.",
"height": 286.0032275416889,
"width": 167.70306616460465
},
"id": "3813357f-d657-4e47-8da7-a452a60ba728",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
465,
-100
]
}
],
"pinData": {},
"connections": {
"n8n Form Trigger": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields": {
"main": [
[
{
"node": "Get Publishers from Netskope",
"type": "main",
"index": 0
}
]
]
},
"Get Publishers from Netskope": {
"main": [
[
{
"node": "Extract Publishers",
"type": "main",
"index": 0
}
]
]
},
"Normalise JSON": {
"main": [
[
{
"node": "Request Approval",
"type": "main",
"index": 0
}
]
]
},
"Extract Publishers": {
"main": [
[
{
"node": "Normalise JSON",
"type": "main",
"index": 0
}
]
]
},
"Create Private App in Netskope": {
"main": [
[
{
"node": "Notify Success",
"type": "main",
"index": 0
}
]
]
},
"Request Approval": {
"main": [
[
{
"node": "Approval Wait",
"type": "main",
"index": 0
}
]
]
},
"Check Response": {
"main": [
[
{
"node": "Create Private App in Netskope",
"type": "main",
"index": 0
}
],
[
{
"node": "Notify Decline",
"type": "main",
"index": 0
}
]
]
},
"Approval Wait": {
"main": [
[
{
"node": "Check Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "79aa997a-a19a-40e0-aa17-85f259571e2c",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "d82986707a1d0ee7f1217f152386e3c6cd26a5ced5beadb45b775f3098d8fb2a"
},
"id": "G7JZ3c4AEmkMeLyr",
"tags": []
}

 

 

Be the first to reply!