Expose ITAM devices on MCP, CLI, and n8n

Devices were only available through GraphQL and the agent API. Add
list/get/revoke/delete/set-owner across MCP, prb, and n8n, with latest
postures nested on list and get responses.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
Ludovic Vielle
2026-07-30 18:20:09 +02:00
parent 36e038f5ff
commit 62e65eccda
26 changed files with 2548 additions and 3 deletions

View File

@@ -130,7 +130,7 @@ This pattern works well for daily compliance standups or overdue-task digests.
The **Probo** node exposes operations across the platform, including:
Access Review, Asset, Audit, Audit Log, Control, Cookie Banner, Cookie Category, Cookie Consent Record, Data, Document, DPIA, Evidence, Finding, Framework, Measure, Obligation, Organization, Processing Activity, Risk, Task, Third Party, Trust Center, User, Vendor, and more.
Access Review, Asset, Audit, Audit Log, Control, Cookie Banner, Cookie Category, Cookie Consent Record, Data, Device, Document, DPIA, Evidence, Finding, Framework, Measure, Obligation, Organization, Processing Activity, Risk, Task, Third Party, Trust Center, User, Vendor, and more.
Use the **Execute** resource to run custom GraphQL queries or mutations when a dedicated operation is not available.

View File

@@ -128,6 +128,11 @@ export class Probo implements INodeType {
value: 'datum',
description: 'Manage data',
},
{
name: 'Device',
value: 'device',
description: 'Manage ITAM devices',
},
{
name: 'Document',
value: 'document',

View File

@@ -0,0 +1,63 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { proboApiRequest } from '../../GenericFunctions';
export const description: INodeProperties[] = [
{
displayName: 'Device ID',
name: 'deviceId',
type: 'string',
displayOptions: {
show: {
resource: ['device'],
operation: ['delete'],
},
},
default: '',
description: 'The ID of the revoked device to delete',
required: true,
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const deviceId = this.getNodeParameter('deviceId', itemIndex) as string;
const query = `
mutation DeleteDevice($input: DeleteDeviceInput!) {
deleteDevice(input: $input) {
deletedDeviceId
}
}
`;
const responseData = await proboApiRequest.call(this, query, {
input: { deviceId },
});
return {
json: responseData,
pairedItem: { item: itemIndex },
};
}

View File

@@ -0,0 +1,131 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { proboApiRequest } from '../../GenericFunctions';
export const description: INodeProperties[] = [
{
displayName: 'Device ID',
name: 'deviceId',
type: 'string',
displayOptions: {
show: {
resource: ['device'],
operation: ['get'],
},
},
default: '',
description: 'The ID of the device',
required: true,
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: ['device'],
operation: ['get'],
},
},
options: [
{
displayName: 'Include Owner',
name: 'includeOwner',
type: 'boolean',
default: false,
description: 'Whether to include owner details in the response',
},
{
displayName: 'Include Postures',
name: 'includePostures',
type: 'boolean',
default: false,
description: 'Whether to include latest posture check results in the response',
},
],
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const deviceId = this.getNodeParameter('deviceId', itemIndex) as string;
const options = this.getNodeParameter('options', itemIndex, {}) as {
includeOwner?: boolean;
includePostures?: boolean;
};
const ownerFragment = options.includeOwner
? `owner {
id
fullName
}`
: '';
const latestPosturesFragment = options.includePostures
? `latestPostures {
id
checkKey
status
value {
kind
text
number
}
observedAt
}`
: '';
const query = `
query GetDevice($deviceId: ID!) {
node(id: $deviceId) {
... on Device {
id
state
hostname
platform
osVersion
agentVersion
serialNumber
hardwareUuid
enrolledAt
lastSeenAt
revokedAt
createdAt
updatedAt
${ownerFragment}
${latestPosturesFragment}
}
}
}
`;
const responseData = await proboApiRequest.call(this, query, { deviceId });
return {
json: responseData,
pairedItem: { item: itemIndex },
};
}

View File

@@ -0,0 +1,183 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import type { INodeProperties, IExecuteFunctions, INodeExecutionData, IDataObject } from 'n8n-workflow';
import { proboApiRequestAllItems } from '../../GenericFunctions';
export const description: INodeProperties[] = [
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'string',
displayOptions: {
show: {
resource: ['device'],
operation: ['getAll'],
},
},
default: '',
description: 'The ID of the organization',
required: true,
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['device'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['device'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
},
default: 50,
description: 'Max number of results to return',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: ['device'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Include Owner',
name: 'includeOwner',
type: 'boolean',
default: false,
description: 'Whether to include owner details in the response',
},
{
displayName: 'Include Postures',
name: 'includePostures',
type: 'boolean',
default: false,
description: 'Whether to include latest posture check results in the response',
},
],
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const organizationId = this.getNodeParameter('organizationId', itemIndex) as string;
const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean;
const limit = this.getNodeParameter('limit', itemIndex, 50) as number;
const options = this.getNodeParameter('options', itemIndex, {}) as {
includeOwner?: boolean;
includePostures?: boolean;
};
const ownerFragment = options.includeOwner
? `owner {
id
fullName
}`
: '';
const latestPosturesFragment = options.includePostures
? `latestPostures {
id
checkKey
status
value {
kind
text
number
}
observedAt
}`
: '';
const query = `
query GetDevices($organizationId: ID!, $first: Int, $after: CursorKey) {
node(id: $organizationId) {
... on Organization {
devices(first: $first, after: $after) {
edges {
node {
id
state
hostname
platform
osVersion
agentVersion
serialNumber
lastSeenAt
enrolledAt
revokedAt
createdAt
updatedAt
${ownerFragment}
${latestPosturesFragment}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`;
const devices = await proboApiRequestAllItems.call(
this,
query,
{ organizationId },
(response) => {
const data = response?.data as IDataObject | undefined;
const node = data?.node as IDataObject | undefined;
return node?.devices as IDataObject | undefined;
},
returnAll,
limit,
);
return {
json: { devices },
pairedItem: { item: itemIndex },
};
}

View File

@@ -0,0 +1,86 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import type { INodeProperties } from 'n8n-workflow';
import * as deleteOp from './delete.operation';
import * as getOp from './get.operation';
import * as getAllOp from './getAll.operation';
import * as revokeOp from './revoke.operation';
import * as setOwnerOp from './setOwner.operation';
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['device'],
},
},
options: [
{
name: 'Delete',
value: 'delete',
description: 'Delete a revoked ITAM device',
action: 'Delete a device',
},
{
name: 'Get',
value: 'get',
description: 'Get an ITAM device',
action: 'Get a device',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many ITAM devices',
action: 'Get many devices',
},
{
name: 'Revoke',
value: 'revoke',
description: 'Revoke an ITAM device enrollment',
action: 'Revoke a device',
},
{
name: 'Set Owner',
value: 'setOwner',
description: 'Set or clear the owner of an ITAM device',
action: 'Set device owner',
},
],
default: 'getAll',
},
...deleteOp.description,
...getOp.description,
...getAllOp.description,
...revokeOp.description,
...setOwnerOp.description,
];
export {
deleteOp as delete,
getOp as get,
getAllOp as getAll,
revokeOp as revoke,
setOwnerOp as setOwner,
};

View File

@@ -0,0 +1,67 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { proboApiRequest } from '../../GenericFunctions';
export const description: INodeProperties[] = [
{
displayName: 'Device ID',
name: 'deviceId',
type: 'string',
displayOptions: {
show: {
resource: ['device'],
operation: ['revoke'],
},
},
default: '',
description: 'The ID of the device to revoke',
required: true,
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const deviceId = this.getNodeParameter('deviceId', itemIndex) as string;
const query = `
mutation RevokeDevice($input: RevokeDeviceInput!) {
revokeDevice(input: $input) {
device {
id
state
revokedAt
}
}
}
`;
const responseData = await proboApiRequest.call(this, query, {
input: { deviceId },
});
return {
json: responseData,
pairedItem: { item: itemIndex },
};
}

View File

@@ -0,0 +1,105 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { proboApiRequest } from '../../GenericFunctions';
export const description: INodeProperties[] = [
{
displayName: 'Device ID',
name: 'deviceId',
type: 'string',
displayOptions: {
show: {
resource: ['device'],
operation: ['setOwner'],
},
},
default: '',
description: 'The ID of the device',
required: true,
},
{
displayName: 'Clear Owner',
name: 'clearOwner',
type: 'boolean',
displayOptions: {
show: {
resource: ['device'],
operation: ['setOwner'],
},
},
default: false,
description: 'Whether to clear the device owner instead of assigning one',
},
{
displayName: 'Owner ID',
name: 'ownerId',
type: 'string',
displayOptions: {
show: {
resource: ['device'],
operation: ['setOwner'],
clearOwner: [false],
},
},
default: '',
description: 'The profile ID of the owner',
required: true,
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const deviceId = this.getNodeParameter('deviceId', itemIndex) as string;
const clearOwner = this.getNodeParameter('clearOwner', itemIndex) as boolean;
const input: { deviceId: string; ownerId: string | null } = {
deviceId,
ownerId: null,
};
if (!clearOwner) {
input.ownerId = this.getNodeParameter('ownerId', itemIndex) as string;
}
const query = `
mutation SetDeviceOwner($input: SetDeviceOwnerInput!) {
setDeviceOwner(input: $input) {
device {
id
owner {
id
fullName
}
}
}
}
`;
const responseData = await proboApiRequest.call(this, query, { input });
return {
json: responseData,
pairedItem: { item: itemIndex },
};
}

View File

@@ -29,6 +29,7 @@ import * as cookieCategory from './cookieCategory';
import * as cookieConsentRecord from './cookieConsentRecord';
import * as trackerPattern from './trackerPattern';
import * as datum from './datum';
import * as device from './device';
import * as document from './document';
import * as dpia from './dpia';
import * as evidence from './evidence';
@@ -73,6 +74,7 @@ export const resources: Record<string, ResourceModule> = {
cookieConsentRecord: cookieConsentRecord as ResourceModule,
trackerPattern: trackerPattern as ResourceModule,
datum: datum as ResourceModule,
device: device as ResourceModule,
document: document as ResourceModule,
dpia: dpia as ResourceModule,
evidence: evidence as ResourceModule,