Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,63 @@
import { randomUUID as uuid } from 'crypto';

import * as dataModel from '@jupiterone/data-model';

import {
createIntegrationEntity,
IntegrationEntityData,
schemaWhitelistedPropertyNames,
schemaWhitelists,
} from '../createIntegrationEntity';

// Inline NHI class schema. The published @jupiterone/data-model package does
// not yet ship NHI (AIASM-15 will publish it). For verification we register an
// equivalent of data-model/packages/jupiterone-data-model/src/class_schemas/NHI.json
// so getSchema('NHI') resolves during these tests.
const NHI_CLASS_SCHEMA = {
$schema: 'http://json-schema.org/draft-07/schema#',
$id: '#NHI',
description: 'Non-Human Identity test fixture mirroring data-model NHI.json',
type: 'object',
allOf: [
{ $ref: '#Entity' },
{
properties: {
nhiType: {
type: 'string',
enum: [
'service_account',
'credential',
'secret',
'oauth_app',
'bot',
'certificate',
'api_key',
'webhook',
'ci_cd_identity',
],
},
isAi: { type: 'boolean' },
aiConfidence: {
type: 'string',
enum: ['confirmed', 'high', 'medium', 'low'],
},
aiPlatform: { type: 'string' },
nhiOwnerStatus: {
type: 'string',
enum: ['assigned', 'unassigned', 'orphaned'],
},
},
required: [],
},
],
};

beforeAll(() => {
if (!dataModel.getSchema('NHI')) {
dataModel.IntegrationSchema.addSchema(NHI_CLASS_SCHEMA);
}
});

const networkSourceData = {
id: 'natural-identifier',
environment: 'production',
Expand Down Expand Up @@ -370,3 +421,211 @@ describe('schema validation off', () => {
).not.toThrow();
});
});

// AIASM-14: Verify multi-class entity validation for NHI combinations.
// These tests prove the SDK already supports `_class: [<Primary>, 'NHI']`
// without code changes — the deliverable is a test suite that pins the
// behavior so future refactors don't regress it.
describe('AIASM-14: multi-class NHI combinations', () => {
const NHI_PROPERTIES = [
'nhiType',
'isAi',
'aiConfidence',
'aiPlatform',
'nhiOwnerStatus',
];

describe('schemaWhitelistedPropertyNames unions properties from each class', () => {
// Entity-base properties present on every J1 class.
const ENTITY_BASE_PROPS = ['id', 'name', 'displayName'];

test.each([
['User', ['username', 'email']],
['AccessKey', ['fingerprint', 'material', 'usage']],
['Application', ['COTS', 'SaaS', 'license']],
// Certificate / Secret extend Entity directly without adding properties
// in @jupiterone/data-model 0.62.0 — assert only Entity-base + NHI.
['Certificate', []],
['Secret', []],
])(
'%s + NHI: includes NHI metadata and primary-class properties',
(primaryClass, primarySpecificProps) => {
const props = schemaWhitelistedPropertyNames([primaryClass, 'NHI']);
expect(props).toIncludeAllMembers(NHI_PROPERTIES);
expect(props).toIncludeAllMembers(ENTITY_BASE_PROPS);
if (primarySpecificProps.length) {
expect(props).toIncludeAllMembers(primarySpecificProps);
}
},
);
});

describe('createIntegrationEntity preserves NHI properties for multi-class entities (BDD 14.5)', () => {
test('User + NHI: keeps nhiType plus standard User fields (BDD 14.1)', () => {
const entity = createIntegrationEntity({
entityData: {
assign: {
_class: ['User', 'NHI'],
_type: 'gh_service_user',
_key: 'gh:svc:1',
},
source: {
id: 'svc-1',
name: 'github-actions-bot',
username: 'gh-actions',
email: 'bot@example.com',
active: true,
nhiType: 'service_account',
isAi: false,
aiConfidence: 'low',
nhiOwnerStatus: 'assigned',
},
},
});

expect(entity._class).toEqual(['User', 'NHI']);
expect(entity).toMatchObject({
username: 'gh-actions',
email: 'bot@example.com',
nhiType: 'service_account',
isAi: false,
aiConfidence: 'low',
nhiOwnerStatus: 'assigned',
});
});

test('AccessKey + NHI: keeps nhiType=credential plus AccessKey-specific fingerprint (BDD 14.2)', () => {
const entity = createIntegrationEntity({
entityData: {
assign: {
_class: ['AccessKey', 'NHI'],
_type: 'aws_iam_access_key',
_key: 'aws:ak:1',
},
source: {
id: 'AKIA-EXAMPLE',
name: 'service-key',
fingerprint: 'sha256:deadbeef',
usage: 'signing',
nhiType: 'credential',
isAi: false,
},
},
});

expect(entity._class).toEqual(['AccessKey', 'NHI']);
expect(entity).toMatchObject({
fingerprint: 'sha256:deadbeef',
usage: 'signing',
nhiType: 'credential',
isAi: false,
});
});

test('Application + NHI: keeps nhiType=oauth_app and AI metadata', () => {
const entity = createIntegrationEntity({
entityData: {
assign: {
_class: ['Application', 'NHI'],
_type: 'okta_oauth_app',
_key: 'okta:app:1',
},
source: {
id: 'okta-app-1',
name: 'Anthropic Claude Bot',
SaaS: true,
license: 'commercial',
nhiType: 'oauth_app',
isAi: true,
aiConfidence: 'confirmed',
aiPlatform: 'anthropic',
},
},
});

expect(entity._class).toEqual(['Application', 'NHI']);
expect(entity).toMatchObject({
SaaS: true,
license: 'commercial',
nhiType: 'oauth_app',
isAi: true,
aiConfidence: 'confirmed',
aiPlatform: 'anthropic',
});
});

test('Certificate + NHI: keeps nhiType=certificate alongside Entity-base properties', () => {
const entity = createIntegrationEntity({
entityData: {
assign: {
_class: ['Certificate', 'NHI'],
_type: 'tls_certificate',
_key: 'cert:1',
},
source: {
id: 'cert-1',
name: 'service.example.com',
description: 'TLS cert for service',
nhiType: 'certificate',
},
},
});

expect(entity._class).toEqual(['Certificate', 'NHI']);
expect(entity).toMatchObject({
name: 'service.example.com',
description: 'TLS cert for service',
nhiType: 'certificate',
});
});

test('Secret + NHI: keeps nhiType=secret alongside Entity-base properties', () => {
const entity = createIntegrationEntity({
entityData: {
assign: {
_class: ['Secret', 'NHI'],
_type: 'vault_secret',
_key: 'secret:1',
},
source: {
id: 'secret-1',
name: 'db-password',
description: 'Production database password',
nhiType: 'secret',
nhiOwnerStatus: 'orphaned',
},
},
});

expect(entity._class).toEqual(['Secret', 'NHI']);
expect(entity).toMatchObject({
name: 'db-password',
description: 'Production database password',
nhiType: 'secret',
nhiOwnerStatus: 'orphaned',
});
});

test('omitting all NHI properties is allowed (BDD 14.4: optional)', () => {
const entity = createIntegrationEntity({
entityData: {
assign: {
_class: ['User', 'NHI'],
_type: 'gh_service_user',
_key: 'gh:svc:2',
},
source: {
id: 'svc-2',
name: 'no-metadata-yet',
username: 'no-meta',
},
},
});

expect(entity._class).toEqual(['User', 'NHI']);
for (const prop of NHI_PROPERTIES) {
expect(prop in entity).toBe(false);
}
});
});
});
Loading
Loading