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

@@ -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,