Skip to content

Commit badcb40

Browse files
committed
Merge remote-tracking branch 'origin/staging' into feat/crowdstrike-depth
# Conflicts: # apps/sim/tools/generated/tool-ids.ts # apps/sim/tools/generated/tool-metadata.ts # apps/sim/tools/generated/tool-outputs.ts
2 parents 241040f + cbbcca9 commit badcb40

110 files changed

Lines changed: 11322 additions & 957 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/components/icons.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8726,6 +8726,33 @@ export function BigQueryIcon(props: SVGProps<SVGSVGElement>) {
87268726
)
87278727
}
87288728

8729+
/**
8730+
* Splunk wordmark. The lettering is drawn with `currentColor` so it stays legible
8731+
* bare in both light and dark mode; the chevron keeps its brand green.
8732+
*/
8733+
export function SplunkIcon(props: SVGProps<SVGSVGElement>) {
8734+
return (
8735+
<svg
8736+
viewBox='0 0 48 48'
8737+
fillRule='evenodd'
8738+
clipRule='evenodd'
8739+
xmlns='http://www.w3.org/2000/svg'
8740+
{...props}
8741+
>
8742+
<path
8743+
fill='currentColor'
8744+
d='M13.432 18L13.5 28 15 28 15 18zM32 18v10.005h1.805c0 0 .015-3.609.035-3.608.019.001 2.69 3.743 2.716 3.733.027-.01 1.599-.576 1.599-.59 0-.014-2.684-3.441-2.684-3.448 0-.006 2.251-2.509 2.231-2.529-.06-.06-1.281-.586-1.314-.566-.018.011-2.582 2.796-2.582 2.796V18H32zM2.099 21.05c-.651.137-1.259.552-1.509 1.031-.23.44-.227 1.043.007 1.491.16.305.625.735 1.348 1.246.782.552 1.078.832 1.153 1.092.111.385-.06.752-.434.931-.187.089-.27.101-.603.086-.433-.02-.729-.132-1.172-.446-.14-.099-.269-.167-.285-.152-.016.016-.155.224-.311.466L.01 27.23l.109.081c.06.045.248.158.422.255 1.187.66 2.944.555 3.835-.229.475-.418.697-1.025.602-1.647-.089-.582-.409-.965-1.417-1.695-.791-.573-1.166-.889-1.253-1.059-.167-.326-.049-.643.307-.827.178-.092.251-.102.608-.087.347.015.453.041.741.179l.335.161.263-.41c.144-.224.252-.426.239-.445-.048-.071-.598-.299-.941-.391C3.402 20.995 2.521 20.961 2.099 21.05zM10.031 21.935c.788.369 1.212 1.528 1.041 2.852-.06.467-.227 1.032-.383 1.296-.159.268-.463.56-.705.675-.444.212-1.107.13-1.523-.187-.577-.442-.893-1.524-.772-2.649.111-1.032.483-1.684 1.133-1.986C9.156 21.781 9.7 21.781 10.031 21.935zM9.313 20.784c-.472.106-.85.319-1.254.705l-.366.35V21H6.814 6v5 5h1.693v-4.08l.324.319c.592.583 1.287.814 2.101.701.532-.074.904-.21 1.3-.475 1.111-.743 1.721-2.221 1.554-3.771-.158-1.467-.912-2.484-2.128-2.871C10.531 20.724 9.679 20.702 9.313 20.784zM16.258 21c0 0 .055 5.284.091 5.438.136.583.525 1.137.964 1.374.403.218.727.287 1.333.284.861-.004 1.386-.211 2.027-.8L21 27v1h1.742l-.023-7H21c0 0 .025 4.347 0 4.5-.1.611-.573 1.109-1.053 1.27-.287.096-.786.174-1.086.127C18.549 26.848 18.033 26.507 18 26v-4.977L16.258 21zM30.742 28c0 0-.055-5.284-.091-5.438-.136-.583-.525-1.137-.964-1.374-.403-.218-.727-.287-1.333-.284-.861.004-1.386.211-2.027.8L26 22v-1h-1.742l.023 7H26c0 0-.025-4.347 0-4.5.1-.611.573-1.109 1.053-1.27.287-.096.661-.17 1.086-.127C28.566 22.147 29.031 22.535 29 23v4.977L30.742 28z'
8745+
/>
8746+
<path
8747+
fill='#77b539'
8748+
fillRule='evenodd'
8749+
clipRule='evenodd'
8750+
d='M40 20.5L40 22 46 24 40 26.601 40 28 47 25 47 23z'
8751+
/>
8752+
</svg>
8753+
)
8754+
}
8755+
87298756
export function SnowflakeIcon(props: SVGProps<SVGSVGElement>) {
87308757
return (
87318758
<svg viewBox='0 0 146.36 139.16' xmlns='http://www.w3.org/2000/svg' {...props}>

apps/docs/components/ui/icon-mapping.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ import {
216216
SmartleadIcon,
217217
SmtpIcon,
218218
SnowflakeIcon,
219+
SplunkIcon,
219220
SportmonksIcon,
220221
SQSIcon,
221222
SquareIcon,
@@ -507,6 +508,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
507508
smartlead: SmartleadIcon,
508509
smtp: SmtpIcon,
509510
snowflake: SnowflakeIcon,
511+
splunk: SplunkIcon,
510512
sportmonks: SportmonksIcon,
511513
sqs: SQSIcon,
512514
square: SquareIcon,

apps/docs/content/docs/en/integrations/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@
230230
"smtp",
231231
"snowflake",
232232
"snowflake-service-account",
233+
"splunk",
233234
"sportmonks",
234235
"sqs",
235236
"square",

apps/docs/content/docs/en/integrations/okta.mdx

Lines changed: 765 additions & 8 deletions
Large diffs are not rendered by default.

apps/docs/content/docs/en/integrations/splunk.mdx

Lines changed: 437 additions & 0 deletions
Large diffs are not rendered by default.

apps/sim/app/api/auth/[...all]/route.test.ts

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('auth catch-all route (DISABLE_AUTH get-session)', () => {
5050
'http://localhost:3000/api/auth/get-session'
5151
)
5252

53-
const res = await GET(req as any)
53+
const res = await GET(req)
5454
const json = await res.json()
5555

5656
expect(handlerMocks.ensureAnonymousUserExists).toHaveBeenCalledTimes(1)
@@ -68,7 +68,7 @@ describe('auth catch-all route (DISABLE_AUTH get-session)', () => {
6868
handlerMocks.betterAuthGET.mockResolvedValueOnce(
6969
new NextResponse(JSON.stringify({ data: { ok: true } }), {
7070
headers: { 'content-type': 'application/json' },
71-
}) as any
71+
})
7272
)
7373

7474
const req = createMockRequest(
@@ -78,7 +78,7 @@ describe('auth catch-all route (DISABLE_AUTH get-session)', () => {
7878
'http://localhost:3000/api/auth/get-session'
7979
)
8080

81-
const res = await GET(req as any)
81+
const res = await GET(req)
8282
const json = await res.json()
8383

8484
expect(handlerMocks.ensureAnonymousUserExists).not.toHaveBeenCalled()
@@ -100,7 +100,7 @@ describe('auth catch-all route organization mutations', () => {
100100
'http://localhost:3000/api/auth/organization/create'
101101
)
102102

103-
const res = await POST(req as any)
103+
const res = await POST(req)
104104
const json = await res.json()
105105

106106
expect(res.status).toBe(404)
@@ -115,7 +115,7 @@ describe('auth catch-all route organization mutations', () => {
115115
handlerMocks.betterAuthPOST.mockResolvedValueOnce(
116116
new NextResponse(JSON.stringify({ data: { ok: true } }), {
117117
headers: { 'content-type': 'application/json' },
118-
}) as any
118+
})
119119
)
120120

121121
const req = createMockRequest(
@@ -125,10 +125,77 @@ describe('auth catch-all route organization mutations', () => {
125125
'http://localhost:3000/api/auth/organization/set-active'
126126
)
127127

128-
const res = await POST(req as any)
128+
const res = await POST(req)
129129
const json = await res.json()
130130

131131
expect(handlerMocks.betterAuthPOST).toHaveBeenCalledTimes(1)
132132
expect(json).toEqual({ data: { ok: true } })
133133
})
134134
})
135+
136+
describe('auth catch-all route SSO provider mutations', () => {
137+
beforeEach(() => {
138+
vi.clearAllMocks()
139+
})
140+
141+
it.each([
142+
'sso/update-provider',
143+
'sso/delete-provider',
144+
'sso/request-domain-verification',
145+
'sso/verify-domain',
146+
])('blocks the plugin-served %s endpoint', async (path) => {
147+
const req = createMockRequest('POST', undefined, {}, `http://localhost:3000/api/auth/${path}`)
148+
149+
const res = await POST(req)
150+
const json = await res.json()
151+
152+
expect(res.status).toBe(404)
153+
expect(handlerMocks.betterAuthPOST).not.toHaveBeenCalled()
154+
expect(json).toEqual({
155+
error: 'SSO provider mutations are handled by application API routes.',
156+
})
157+
})
158+
159+
it.each([
160+
'sso/saml2/callback/acme',
161+
'sso/saml2/sp/acs/acme',
162+
'sso/saml2/sp/slo/acme',
163+
'sso/saml2/logout/acme',
164+
])('allows the SAML protocol endpoint %s', async (path) => {
165+
const { NextResponse } = await import('next/server')
166+
handlerMocks.betterAuthPOST.mockResolvedValueOnce(
167+
new NextResponse(JSON.stringify({ data: { ok: true } }), {
168+
headers: { 'content-type': 'application/json' },
169+
})
170+
)
171+
172+
const req = createMockRequest('POST', undefined, {}, `http://localhost:3000/api/auth/${path}`)
173+
174+
const res = await POST(req)
175+
const json = await res.json()
176+
177+
expect(handlerMocks.betterAuthPOST).toHaveBeenCalledTimes(1)
178+
expect(json).toEqual({ data: { ok: true } })
179+
})
180+
181+
it('leaves the SSO sign-in endpoint reachable', async () => {
182+
const { NextResponse } = await import('next/server')
183+
handlerMocks.betterAuthPOST.mockResolvedValueOnce(
184+
new NextResponse(JSON.stringify({ data: { url: 'https://idp.example.com' } }), {
185+
headers: { 'content-type': 'application/json' },
186+
})
187+
)
188+
189+
const req = createMockRequest(
190+
'POST',
191+
undefined,
192+
{},
193+
'http://localhost:3000/api/auth/sign-in/sso'
194+
)
195+
196+
const res = await POST(req)
197+
198+
expect(handlerMocks.betterAuthPOST).toHaveBeenCalledTimes(1)
199+
expect(await res.json()).toEqual({ data: { url: 'https://idp.example.com' } })
200+
})
201+
})

apps/sim/app/api/auth/[...all]/route.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ export const dynamic = 'force-dynamic'
1010
const { GET: betterAuthGET, POST: betterAuthPOST } = toNextJsHandler(auth.handler)
1111
const SAFE_ORGANIZATION_POST_PATHS = new Set(['organization/check-slug', 'organization/set-active'])
1212

13+
/**
14+
* SAML protocol endpoints the IdP posts to (`saml2/callback/:id`,
15+
* `saml2/sp/acs/:id`, `saml2/sp/slo/:id`, `saml2/logout/:id`). These are the
16+
* only SSO paths the plugin must keep serving on POST — every other SSO POST
17+
* endpoint it registers is a provider mutation.
18+
*/
19+
const SAML_PROTOCOL_POST_PREFIX = 'sso/saml2/'
20+
1321
function getAuthPath(request: NextRequest): string {
1422
const pathname = request.nextUrl?.pathname ?? new URL(request.url).pathname
1523
return pathname.replace('/api/auth/', '')
@@ -19,6 +27,30 @@ function isBlockedOrganizationMutationPath(path: string): boolean {
1927
return path.startsWith('organization/') && !SAFE_ORGANIZATION_POST_PATHS.has(path)
2028
}
2129

30+
/**
31+
* SSO provider configuration is owned by `/api/auth/sso/register`, which proves
32+
* domain ownership before granting trust and restricts the attribute mapping to
33+
* `id`/`email`/`name`/`image`. The plugin's own `sso/update-provider` bypasses
34+
* both: it is gated only on provider ownership and merges the caller's config,
35+
* so a provider owner could add `mapping.emailVerified` — a change the plugin's
36+
* identity-boundary guard does not consider, so it never trips the linked-account
37+
* conflict — and then assert an arbitrary victim's email as verified to auto-link
38+
* into their account. `sso/delete-provider` likewise lets an owner drop a login
39+
* path outside the application's flow.
40+
*
41+
* `trustEmailVerified: false` independently defuses that claim, so these two
42+
* guards are layered, not redundant: this one keeps provider configuration
43+
* owned by the register route (which alone proves domain ownership) and is what
44+
* stops the mapping rewrite from becoming live again if that option is ever
45+
* reconsidered.
46+
*
47+
* Deny-by-default rather than a blocklist so a future plugin version cannot
48+
* introduce another unshadowed provider mutation.
49+
*/
50+
function isBlockedSsoMutationPath(path: string): boolean {
51+
return path.startsWith('sso/') && !path.startsWith(SAML_PROTOCOL_POST_PREFIX)
52+
}
53+
2254
export const GET = withRouteHandler(async (request: NextRequest) => {
2355
const path = getAuthPath(request)
2456

@@ -40,5 +72,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
4072
)
4173
}
4274

75+
if (isBlockedSsoMutationPath(path)) {
76+
return NextResponse.json(
77+
{ error: 'SSO provider mutations are handled by application API routes.' },
78+
{ status: 404 }
79+
)
80+
}
81+
4382
return betterAuthPOST(request)
4483
})

0 commit comments

Comments
 (0)