chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,271 @@
/**
* @vitest-environment node
*/
import { createMockRequest, hybridAuthMockFns } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { fetchMock } = vi.hoisted(() => ({
fetchMock: vi.fn(),
}))
import { POST } from '@/app/api/tools/crowdstrike/query/route'
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json' },
})
}
const sensorResource = {
agent_version: '6.1.0',
cid: 'cid-1',
device_id: 'sensor-1',
heartbeat_time: 1700,
hostname: 'host-1',
idp_policy_id: 'policy-1',
idp_policy_name: 'Default Policy',
kerberos_config: 'configured',
ldap_config: 'configured',
ldaps_config: 'configured',
local_ip: '10.0.0.1',
machine_domain: 'corp.local',
ntlm_config: 'configured',
os_version: 'Windows Server 2022',
rdp_to_dc_config: 'configured',
smb_to_dc_config: 'configured',
status: 'protected',
status_causes: ['healthy'],
ti_enabled: 'enabled',
}
const normalizedSensor = {
agentVersion: '6.1.0',
cid: 'cid-1',
deviceId: 'sensor-1',
heartbeatTime: 1700,
hostname: 'host-1',
idpPolicyId: 'policy-1',
idpPolicyName: 'Default Policy',
ipAddress: '10.0.0.1',
kerberosConfig: 'configured',
ldapConfig: 'configured',
ldapsConfig: 'configured',
machineDomain: 'corp.local',
ntlmConfig: 'configured',
osVersion: 'Windows Server 2022',
rdpToDcConfig: 'configured',
smbToDcConfig: 'configured',
status: 'protected',
statusCauses: ['healthy'],
tiEnabled: 'enabled',
}
describe('CrowdStrike query route', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.stubGlobal('fetch', fetchMock)
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({
success: true,
userId: 'user-123',
authType: 'internal_jwt',
})
})
it('hydrates sensor details after querying sensor ids', async () => {
fetchMock
.mockResolvedValueOnce(jsonResponse({ access_token: 'token-123' }))
.mockResolvedValueOnce(
jsonResponse({
meta: { pagination: { expires_at: 111, limit: 1, offset: 0, total: 1 } },
resources: ['sensor-1'],
})
)
.mockResolvedValueOnce(
jsonResponse({
resources: [sensorResource],
})
)
const request = createMockRequest('POST', {
clientId: 'client-id',
clientSecret: 'client-secret',
cloud: 'us-1',
limit: 1,
operation: 'crowdstrike_query_sensors',
})
const response = await POST(request)
const data = await response.json()
expect(response.status).toBe(200)
expect(fetchMock).toHaveBeenCalledTimes(3)
expect(fetchMock.mock.calls[1]?.[0]).toBe(
'https://api.crowdstrike.com/identity-protection/queries/devices/v1?limit=1'
)
expect(fetchMock.mock.calls[2]?.[0]).toBe(
'https://api.crowdstrike.com/identity-protection/entities/devices/GET/v1'
)
expect(fetchMock.mock.calls[2]?.[1]).toMatchObject({
body: JSON.stringify({ ids: ['sensor-1'] }),
method: 'POST',
})
expect(data.output).toEqual({
count: 1,
pagination: {
limit: 1,
offset: 0,
total: 1,
},
sensors: [normalizedSensor],
})
})
it('fetches sensor details directly from device ids', async () => {
fetchMock
.mockResolvedValueOnce(jsonResponse({ access_token: 'token-123' }))
.mockResolvedValueOnce(
jsonResponse({
resources: [sensorResource],
})
)
const request = createMockRequest('POST', {
clientId: 'client-id',
clientSecret: 'client-secret',
cloud: 'us-1',
ids: ['sensor-1'],
operation: 'crowdstrike_get_sensor_details',
})
const response = await POST(request)
const data = await response.json()
expect(response.status).toBe(200)
expect(fetchMock).toHaveBeenCalledTimes(2)
expect(fetchMock.mock.calls[1]?.[0]).toBe(
'https://api.crowdstrike.com/identity-protection/entities/devices/GET/v1'
)
expect(fetchMock.mock.calls[1]?.[1]).toMatchObject({
body: JSON.stringify({ ids: ['sensor-1'] }),
method: 'POST',
})
expect(data.output).toEqual({
count: 1,
pagination: null,
sensors: [normalizedSensor],
})
})
it('normalizes sensor aggregate results', async () => {
fetchMock
.mockResolvedValueOnce(jsonResponse({ access_token: 'token-123' }))
.mockResolvedValueOnce(
jsonResponse({
resources: [
{
buckets: [
{
count: 2,
key_as_string: 'protected',
sub_aggregates: [
{
buckets: [
{
count: 2,
key_as_string: 'corp.local',
value: 2,
value_as_string: '2',
},
],
doc_count_error_upper_bound: 0,
name: 'machine_domain_counts',
sum_other_doc_count: 0,
},
],
value: 2,
value_as_string: '2',
},
],
doc_count_error_upper_bound: 0,
name: 'status_counts',
sum_other_doc_count: 0,
},
],
})
)
const aggregateQuery = {
field: 'status',
name: 'status_counts',
size: 10,
type: 'terms',
}
const request = createMockRequest('POST', {
aggregateQuery,
clientId: 'client-id',
clientSecret: 'client-secret',
cloud: 'us-1',
operation: 'crowdstrike_get_sensor_aggregates',
})
const response = await POST(request)
const data = await response.json()
expect(response.status).toBe(200)
expect(fetchMock).toHaveBeenCalledTimes(2)
expect(fetchMock.mock.calls[1]?.[0]).toBe(
'https://api.crowdstrike.com/identity-protection/aggregates/devices/GET/v1'
)
expect(fetchMock.mock.calls[1]?.[1]).toMatchObject({
body: JSON.stringify(aggregateQuery),
method: 'POST',
})
expect(data.output).toEqual({
aggregates: [
{
buckets: [
{
count: 2,
from: null,
keyAsString: 'protected',
label: null,
stringFrom: null,
stringTo: null,
subAggregates: [
{
buckets: [
{
count: 2,
from: null,
keyAsString: 'corp.local',
label: null,
stringFrom: null,
stringTo: null,
subAggregates: [],
to: null,
value: 2,
valueAsString: '2',
},
],
docCountErrorUpperBound: 0,
name: 'machine_domain_counts',
sumOtherDocCount: 0,
},
],
to: null,
value: 2,
valueAsString: '2',
},
],
docCountErrorUpperBound: 0,
name: 'status_counts',
sumOtherDocCount: 0,
},
],
count: 1,
})
})
})
@@ -0,0 +1,410 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { isRecordLike } from '@sim/utils/object'
import { type NextRequest, NextResponse } from 'next/server'
import { crowdstrikeQueryContract } from '@/lib/api/contracts/tools/crowdstrike'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type {
CrowdStrikeAggregateQuery,
CrowdStrikeBaseParams,
CrowdStrikeCloud,
CrowdStrikeQuerySensorsParams,
CrowdStrikeSensorAggregateBucket,
CrowdStrikeSensorAggregateResult,
} from '@/tools/crowdstrike/types'
const logger = createLogger('CrowdStrikeIdentityProtectionAPI')
type JsonRecord = Record<string, unknown>
function getCloudBaseUrl(cloud: CrowdStrikeCloud): string {
const cloudMap: Record<CrowdStrikeCloud, string> = {
'eu-1': 'https://api.eu-1.crowdstrike.com',
'us-1': 'https://api.crowdstrike.com',
'us-2': 'https://api.us-2.crowdstrike.com',
'us-gov-1': 'https://api.laggar.gcw.crowdstrike.com',
'us-gov-2': 'https://api.us-gov-2.crowdstrike.mil',
}
return cloudMap[cloud]
}
function getString(value: unknown): string | null {
return typeof value === 'string' ? value : null
}
function getNumber(value: unknown): number | null {
return typeof value === 'number' ? value : null
}
function getStringArray(value: unknown): string[] {
if (!Array.isArray(value)) {
return []
}
return value.filter((entry): entry is string => typeof entry === 'string')
}
function getRecordArray(value: unknown): JsonRecord[] {
if (!Array.isArray(value)) {
return []
}
return value.filter(isRecordLike)
}
function getResourcesArray(data: unknown): unknown[] {
const root = getResponseRoot(data)
if (!isRecordLike(root) || !Array.isArray(root.resources)) {
return []
}
return root.resources
}
function getRecordResources(data: unknown): JsonRecord[] {
return getResourcesArray(data).filter(isRecordLike)
}
function getStringResources(data: unknown): string[] {
return getStringArray(getResourcesArray(data))
}
function getResponseRoot(data: unknown): unknown {
if (!isRecordLike(data)) {
return null
}
if (isRecordLike(data.body)) {
return data.body
}
return data
}
function getPagination(data: unknown) {
const root = getResponseRoot(data)
if (!isRecordLike(root) || !isRecordLike(root.meta) || !isRecordLike(root.meta.pagination)) {
return null
}
return {
limit: getNumber(root.meta.pagination.limit),
offset: getNumber(root.meta.pagination.offset),
total: getNumber(root.meta.pagination.total),
}
}
function getErrorMessage(data: unknown, fallback: string): string {
if (!isRecordLike(data)) {
return fallback
}
const errors = Array.isArray(data.errors) ? data.errors : []
const firstError = errors[0]
if (isRecordLike(firstError)) {
const firstMessage = getString(firstError.message) ?? getString(firstError.code)
if (firstMessage) {
return firstMessage
}
}
return (
getString(data.message) ??
getString(data.error_description) ??
getString(data.error) ??
fallback
)
}
function buildQueryUrl(baseUrl: string, params: CrowdStrikeQuerySensorsParams): string {
const url = new URL(baseUrl)
url.pathname = '/identity-protection/queries/devices/v1'
if (params.filter) {
url.searchParams.set('filter', params.filter)
}
if (params.limit != null) {
url.searchParams.set('limit', params.limit.toString())
}
if (params.offset != null) {
url.searchParams.set('offset', params.offset.toString())
}
if (params.sort) {
url.searchParams.set('sort', params.sort)
}
return url.toString()
}
function buildSensorDetailsUrl(baseUrl: string): string {
const url = new URL(baseUrl)
url.pathname = '/identity-protection/entities/devices/GET/v1'
return url.toString()
}
function buildSensorAggregatesUrl(baseUrl: string): string {
const url = new URL(baseUrl)
url.pathname = '/identity-protection/aggregates/devices/GET/v1'
return url.toString()
}
async function getAccessToken(params: CrowdStrikeBaseParams): Promise<string> {
const baseUrl = getCloudBaseUrl(params.cloud)
const response = await fetch(`${baseUrl}/oauth2/token`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: params.clientId,
client_secret: params.clientSecret,
grant_type: 'client_credentials',
}).toString(),
cache: 'no-store',
})
const data: unknown = await response.json().catch(() => null)
if (!response.ok) {
throw new Error(getErrorMessage(data, 'Failed to authenticate with CrowdStrike'))
}
if (!isRecordLike(data) || typeof data.access_token !== 'string') {
throw new Error('CrowdStrike authentication did not return an access token')
}
return data.access_token
}
function normalizeSensor(resource: JsonRecord) {
return {
agentVersion: getString(resource.agent_version),
cid: getString(resource.cid),
deviceId: getString(resource.device_id),
heartbeatTime: getNumber(resource.heartbeat_time),
hostname: getString(resource.hostname),
idpPolicyId: getString(resource.idp_policy_id),
idpPolicyName: getString(resource.idp_policy_name),
ipAddress: getString(resource.local_ip),
kerberosConfig: getString(resource.kerberos_config),
ldapConfig: getString(resource.ldap_config),
ldapsConfig: getString(resource.ldaps_config),
machineDomain: getString(resource.machine_domain),
ntlmConfig: getString(resource.ntlm_config),
osVersion: getString(resource.os_version),
rdpToDcConfig: getString(resource.rdp_to_dc_config),
smbToDcConfig: getString(resource.smb_to_dc_config),
status: getString(resource.status),
statusCauses: getStringArray(resource.status_causes),
tiEnabled: getString(resource.ti_enabled),
}
}
function normalizeSensorsOutput(data: unknown, paginationData?: unknown) {
const sensors = getRecordResources(data).map(normalizeSensor)
return {
count: sensors.length,
pagination: paginationData == null ? null : getPagination(paginationData),
sensors,
}
}
function normalizeAggregationResult(resource: JsonRecord): CrowdStrikeSensorAggregateResult {
return {
buckets: getRecordArray(resource.buckets).map(normalizeAggregationBucket),
docCountErrorUpperBound: getNumber(resource.doc_count_error_upper_bound),
name: getString(resource.name),
sumOtherDocCount: getNumber(resource.sum_other_doc_count),
}
}
function normalizeAggregationBucket(resource: JsonRecord): CrowdStrikeSensorAggregateBucket {
return {
count: getNumber(resource.count),
from: getNumber(resource.from),
keyAsString: getString(resource.key_as_string),
label: isRecordLike(resource.label) ? resource.label : null,
stringFrom: getString(resource.string_from),
stringTo: getString(resource.string_to),
subAggregates: getRecordArray(resource.sub_aggregates).map(normalizeAggregationResult),
to: getNumber(resource.to),
value: getNumber(resource.value),
valueAsString: getString(resource.value_as_string),
}
}
function normalizeAggregatesOutput(data: unknown) {
const aggregates = getRecordResources(data).map(normalizeAggregationResult)
return {
aggregates,
count: aggregates.length,
}
}
async function postCrowdStrikeJson(
url: string,
accessToken: string,
body: JsonRecord | CrowdStrikeAggregateQuery
) {
return fetch(url, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
cache: 'no-store',
})
}
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json(
{ success: false, error: authResult.error || 'Unauthorized' },
{ status: 401 }
)
}
try {
const parsed = await parseRequest(
crowdstrikeQueryContract,
request,
{},
{
validationErrorResponse: (error) =>
NextResponse.json(
{
success: false,
error: getValidationErrorMessage(error, 'Invalid request data'),
details: error.issues,
},
{ status: 400 }
),
}
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
const baseUrl = getCloudBaseUrl(params.cloud)
const accessToken = await getAccessToken(params)
logger.info(`[${requestId}] CrowdStrike request`, {
cloud: params.cloud,
operation: params.operation,
})
if (params.operation === 'crowdstrike_query_sensors') {
const queryResponse = await fetch(buildQueryUrl(baseUrl, params), {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
cache: 'no-store',
})
const queryData: unknown = await queryResponse.json().catch(() => null)
if (!queryResponse.ok) {
return NextResponse.json(
{
success: false,
error: getErrorMessage(queryData, 'CrowdStrike request failed'),
},
{ status: queryResponse.status }
)
}
const ids = getStringResources(queryData)
if (ids.length === 0) {
return NextResponse.json({
success: true,
output: normalizeSensorsOutput({ resources: [] }, queryData),
})
}
const detailResponse = await postCrowdStrikeJson(
buildSensorDetailsUrl(baseUrl),
accessToken,
{ ids }
)
const detailData: unknown = await detailResponse.json().catch(() => null)
if (!detailResponse.ok) {
return NextResponse.json(
{
success: false,
error: getErrorMessage(detailData, 'Failed to fetch CrowdStrike sensor details'),
},
{ status: detailResponse.status }
)
}
return NextResponse.json({
success: true,
output: normalizeSensorsOutput(detailData, queryData),
})
}
if (params.operation === 'crowdstrike_get_sensor_details') {
const detailResponse = await postCrowdStrikeJson(
buildSensorDetailsUrl(baseUrl),
accessToken,
{ ids: params.ids }
)
const detailData: unknown = await detailResponse.json().catch(() => null)
if (!detailResponse.ok) {
return NextResponse.json(
{
success: false,
error: getErrorMessage(detailData, 'Failed to fetch CrowdStrike sensor details'),
},
{ status: detailResponse.status }
)
}
return NextResponse.json({
success: true,
output: normalizeSensorsOutput(detailData),
})
}
const aggregateResponse = await postCrowdStrikeJson(
buildSensorAggregatesUrl(baseUrl),
accessToken,
params.aggregateQuery
)
const aggregateData: unknown = await aggregateResponse.json().catch(() => null)
if (!aggregateResponse.ok) {
return NextResponse.json(
{
success: false,
error: getErrorMessage(aggregateData, 'Failed to fetch CrowdStrike sensor aggregates'),
},
{ status: aggregateResponse.status }
)
}
return NextResponse.json({
success: true,
output: normalizeAggregatesOutput(aggregateData),
})
} catch (error) {
const message = toError(error).message
logger.error(`[${requestId}] CrowdStrike request failed`, { error: message })
return NextResponse.json({ success: false, error: message }, { status: 500 })
}
})