feat: forward request metadata to CrowdStrike AIDR

Forward additional context to CrowdStrike AIDR's guard_chat_completions
endpoint:

 - `user_id` from `metadata._user` or `metadata.user_id`
 - `llm_provider` from the request provider
 - `model` from the request body
 - `extra_info.user_name` from `metadata._user_name` or `metadata.user_name`
This commit is contained in:
Kenan Yildirim
2026-05-11 16:28:33 -04:00
parent 351692fd92
commit b341133ff5
3 changed files with 146 additions and 13 deletions
+128 -11
View File
@@ -1,15 +1,14 @@
import { handler } from './guardChatCompletion';
import testCredsFile from './.creds.json';
import { HookEventType, PluginContext } from '../types';
import * as utils from '../utils';
const options = {
env: {},
};
const testCreds = {
apiKey: testCredsFile.apiKey,
baseUrl: testCredsFile.baseUrl,
blockApiKey: testCredsFile.blockApiKey,
redactApiKey: testCredsFile.redactApiKey,
};
describe('AIDR Handlers', () => {
@@ -75,10 +74,7 @@ describe('AIDR Handlers', () => {
};
const eventType = 'beforeRequestHook';
const parameters = {
credentials: {
baseUrl: testCreds.baseUrl,
apiKey: testCreds.blockApiKey,
},
credentials: testCreds,
};
const result = await handler(context, parameters, eventType, options);
expect(result.error).toBeNull();
@@ -102,10 +98,7 @@ describe('AIDR Handlers', () => {
};
const eventType = 'beforeRequestHook';
const parameters = {
credentials: {
baseUrl: testCreds.baseUrl,
apiKey: testCreds.redactApiKey,
},
credentials: testCreds,
};
const result = await handler(context, parameters, eventType, options);
expect(result.error).toBeNull();
@@ -118,3 +111,127 @@ describe('AIDR Handlers', () => {
);
});
});
describe('AIDR request body metadata', () => {
const parameters = {
credentials: {
baseUrl: 'https://aidr.example.test',
apiKey: 'test-key',
},
};
let postSpy: jest.SpyInstance;
beforeEach(() => {
postSpy = jest.spyOn(utils, 'post').mockResolvedValue({
status: 'Success',
result: { blocked: false, transformed: false, policy: 'test-policy' },
});
});
afterEach(() => {
postSpy.mockRestore();
});
it('forwards user_id, user_name, llm_provider, and model when available', async () => {
const context = {
provider: 'openai',
metadata: {
_user: 'alice',
_user_name: 'Alice Example',
},
request: {
json: {
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'hello' }],
},
},
};
await handler(context, parameters, 'beforeRequestHook');
expect(postSpy).toHaveBeenCalledTimes(1);
const body = postSpy.mock.calls[0][1] as Record<string, any>;
expect(body).toMatchObject({
event_type: 'input',
app_id: 'Portkey AI Gateway',
user_id: 'alice',
llm_provider: 'openai',
model: 'gpt-4o-mini',
extra_info: { user_name: 'Alice Example' },
});
});
it('prefers _user/_user_name over user_id/user_name fallbacks', async () => {
const context = {
provider: 'anthropic',
metadata: {
_user: 'preferred-id',
user_id: 'fallback-id',
_user_name: 'Preferred Name',
user_name: 'Fallback Name',
},
request: { json: { model: 'claude-3-5-sonnet', messages: [] } },
};
await handler(context, parameters, 'beforeRequestHook');
const body = postSpy.mock.calls[0][1] as Record<string, any>;
expect(body.user_id).toBe('preferred-id');
expect(body.extra_info.user_name).toBe('Preferred Name');
});
it('falls back to user_id/user_name when underscore keys are missing', async () => {
const context = {
provider: 'openai',
metadata: {
user_id: 'bob',
user_name: 'Bob Example',
},
request: { json: { model: 'gpt-4o', messages: [] } },
};
await handler(context, parameters, 'beforeRequestHook');
const body = postSpy.mock.calls[0][1] as Record<string, any>;
expect(body.user_id).toBe('bob');
expect(body.extra_info.user_name).toBe('Bob Example');
});
it('omits metadata fields when no values are available', async () => {
const context = {
request: { json: { messages: [] } },
};
await handler(context, parameters, 'beforeRequestHook');
const body = postSpy.mock.calls[0][1] as Record<string, any>;
expect(body).toEqual({
guard_input: { messages: [] },
event_type: 'input',
app_id: 'Portkey AI Gateway',
extra_info: {},
});
expect(body).not.toHaveProperty('user_id');
expect(body).not.toHaveProperty('llm_provider');
expect(body).not.toHaveProperty('model');
});
it('uses output event_type for afterRequestHook and still includes metadata', async () => {
const context = {
provider: 'bedrock',
metadata: { _user: 'charlie' },
request: { json: { model: 'claude-3-haiku', messages: [] } },
response: { json: { choices: [{ message: { content: 'hi' } }] } },
};
await handler(context, parameters, 'afterRequestHook');
const body = postSpy.mock.calls[0][1] as Record<string, any>;
expect(body.event_type).toBe('output');
expect(body.user_id).toBe('charlie');
expect(body.llm_provider).toBe('bedrock');
expect(body.model).toBe('claude-3-haiku');
expect(body.guard_input).toEqual(context.response.json);
});
});
@@ -37,13 +37,25 @@ export const handler: PluginHandler = async (
const json = context[target].json;
const aidrEventType = target === 'request' ? 'input' : 'output';
const requestBody: object = {
const metadata = context.metadata ?? {};
const userId = metadata._user ?? metadata.user_id;
const userName = metadata._user_name ?? metadata.user_name;
const model = context.request?.json?.model;
const requestBody: Record<string, any> = {
guard_input: json,
event_type: aidrEventType,
app_id: 'Portkey AI Gateway',
// TODO: Add as much other metadata as we have
extra_info: {},
};
if (userId) requestBody.user_id = userId;
if (context.provider) requestBody.llm_provider = context.provider;
if (model) requestBody.model = model;
if (userName) {
requestBody.extra_info = { ...requestBody.extra_info, user_name: userName };
}
const requestOptions: object = {
headers: {
'Content-Type': 'application/json',
+4
View File
@@ -66,6 +66,7 @@ import { handler as javelinguardrails } from './javelin/guardrails';
import { handler as f5GuardrailsScan } from './f5-guardrails/scan';
import { handler as azureShieldPrompt } from './azure/shieldPrompt';
import { handler as azureProtectedMaterial } from './azure/protectedMaterial';
import { handler as crowdstrikeAidrGuardChatCompletions } from './crowdstrike-aidr/guardChatCompletion';
export const plugins = {
default: {
@@ -176,4 +177,7 @@ export const plugins = {
'f5-guardrails': {
scan: f5GuardrailsScan,
},
'crowdstrike-aidr': {
guardChatCompletions: crowdstrikeAidrGuardChatCompletions,
},
};