From d5e99d7fc624b24750c7a3a0dff9dab348115cbf Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 4 Sep 2025 03:01:33 -0400 Subject: [PATCH 01/10] Allow app config override for encryptWithRootEncryptionKey --- backend/src/lib/crypto/cryptography/crypto.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/lib/crypto/cryptography/crypto.ts b/backend/src/lib/crypto/cryptography/crypto.ts index b8fc45645..05ea315cd 100644 --- a/backend/src/lib/crypto/cryptography/crypto.ts +++ b/backend/src/lib/crypto/cryptography/crypto.ts @@ -250,8 +250,11 @@ const cryptographyFactory = () => { }; }; - const encryptWithRootEncryptionKey = (data: string) => { - const appCfg = getConfig(); + const encryptWithRootEncryptionKey = ( + data: string, + appCfgOverride?: Pick + ) => { + const appCfg = appCfgOverride || getConfig(); const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY; const encryptionKey = appCfg.ENCRYPTION_KEY; From fcb931670a6823367e8b8cf2b60dc0afaf2c2783 Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 4 Sep 2025 15:54:46 -0400 Subject: [PATCH 02/10] feat(audit-log-stream): backend rework --- backend/src/@types/fastify.d.ts | 2 +- .../20250903191434_audit-log-stream-v2.ts | 210 +++++++++++ backend/src/db/schemas/audit-log-streams.ts | 8 +- .../ee/routes/v1/audit-log-stream-router.ts | 215 ----------- .../audit-log-stream-endpoints.ts | 142 ++++++++ .../audit-log-stream-router.ts | 73 ++++ .../v1/audit-log-stream-routers/index.ts | 48 +++ backend/src/ee/routes/v1/index.ts | 16 +- .../audit-log-stream-enums.ts | 5 + .../audit-log-stream-factory.ts | 13 + .../audit-log-stream/audit-log-stream-fns.ts | 85 ++++- .../audit-log-stream-service.ts | 339 ++++++++---------- .../audit-log-stream-types.ts | 70 ++-- .../custom/custom-provider-factory.ts | 67 ++++ .../custom/custom-provider-fns.ts | 8 + .../custom/custom-provider-schemas.ts | 32 ++ .../custom/custom-provider-types.ts | 7 + .../datadog/datadog-provider-factory.ts | 60 ++++ .../datadog/datadog-provider-fns.ts | 8 + .../datadog/datadog-provider-schemas.ts | 29 ++ .../datadog/datadog-provider-types.ts | 7 + .../splunk/splunk-provider-factory.ts | 84 +++++ .../splunk/splunk-provider-fns.ts | 8 + .../splunk/splunk-provider-schemas.ts | 25 ++ .../splunk/splunk-provider-types.ts | 7 + .../ee/services/audit-log/audit-log-queue.ts | 69 +--- backend/src/server/routes/index.ts | 14 +- backend/src/server/routes/sanitizedSchemas.ts | 7 - 28 files changed, 1120 insertions(+), 538 deletions(-) create mode 100644 backend/src/db/migrations/20250903191434_audit-log-stream-v2.ts delete mode 100644 backend/src/ee/routes/v1/audit-log-stream-router.ts create mode 100644 backend/src/ee/routes/v1/audit-log-stream-routers/audit-log-stream-endpoints.ts create mode 100644 backend/src/ee/routes/v1/audit-log-stream-routers/audit-log-stream-router.ts create mode 100644 backend/src/ee/routes/v1/audit-log-stream-routers/index.ts create mode 100644 backend/src/ee/services/audit-log-stream/audit-log-stream-enums.ts create mode 100644 backend/src/ee/services/audit-log-stream/audit-log-stream-factory.ts create mode 100644 backend/src/ee/services/audit-log-stream/custom/custom-provider-factory.ts create mode 100644 backend/src/ee/services/audit-log-stream/custom/custom-provider-fns.ts create mode 100644 backend/src/ee/services/audit-log-stream/custom/custom-provider-schemas.ts create mode 100644 backend/src/ee/services/audit-log-stream/custom/custom-provider-types.ts create mode 100644 backend/src/ee/services/audit-log-stream/datadog/datadog-provider-factory.ts create mode 100644 backend/src/ee/services/audit-log-stream/datadog/datadog-provider-fns.ts create mode 100644 backend/src/ee/services/audit-log-stream/datadog/datadog-provider-schemas.ts create mode 100644 backend/src/ee/services/audit-log-stream/datadog/datadog-provider-types.ts create mode 100644 backend/src/ee/services/audit-log-stream/splunk/splunk-provider-factory.ts create mode 100644 backend/src/ee/services/audit-log-stream/splunk/splunk-provider-fns.ts create mode 100644 backend/src/ee/services/audit-log-stream/splunk/splunk-provider-schemas.ts create mode 100644 backend/src/ee/services/audit-log-stream/splunk/splunk-provider-types.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index c25d8d4d1..95de82591 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -7,7 +7,7 @@ import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-app import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-types"; import { TAssumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-types"; import { TAuditLogServiceFactory, TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; -import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-types"; +import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-types"; import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service"; import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-types"; diff --git a/backend/src/db/migrations/20250903191434_audit-log-stream-v2.ts b/backend/src/db/migrations/20250903191434_audit-log-stream-v2.ts new file mode 100644 index 000000000..21d652c86 --- /dev/null +++ b/backend/src/db/migrations/20250903191434_audit-log-stream-v2.ts @@ -0,0 +1,210 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { crypto } from "@app/lib/crypto/cryptography"; +import { KmsDataKey } from "@app/services/kms/kms-types"; +import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; + +import { SecretKeyEncoding, TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.AuditLogStream)) { + const hasProvider = await knex.schema.hasColumn(TableName.AuditLogStream, "provider"); + const hasEncryptedCredentials = await knex.schema.hasColumn(TableName.AuditLogStream, "encryptedCredentials"); + + await knex.schema.alterTable(TableName.AuditLogStream, (t) => { + if (!hasProvider) t.string("provider").notNullable().defaultTo("custom"); + if (!hasEncryptedCredentials) t.binary("encryptedCredentials"); + + // This column will no longer be used but we're not dropping it so that we can have a backup in case the migration goes wrong + t.string("url").nullable().alter(); + }); + + const superAdminDAL = superAdminDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL); + const keyStore = inMemoryKeyStore(); + + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const logStreams = await knex(TableName.AuditLogStream).select( + "id", + "orgId", + + "url", + "encryptedHeadersAlgorithm", + "encryptedHeadersCiphertext", + "encryptedHeadersIV", + "encryptedHeadersKeyEncoding", + "encryptedHeadersTag" + ); + + const updatedLogStreams = await Promise.all( + logStreams.map(async (el) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(el.orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.Organization, + orgId: el.orgId + }, + knex + ); + orgEncryptionRingBuffer.push(el.orgId, orgKmsService); + } + + const provider = "custom"; + let credentials; + + if ( + el.encryptedHeadersTag && + el.encryptedHeadersIV && + el.encryptedHeadersCiphertext && + el.encryptedHeadersKeyEncoding + ) { + const decryptedHeaders = crypto + .encryption() + .symmetric() + .decryptWithRootEncryptionKey({ + tag: el.encryptedHeadersTag, + iv: el.encryptedHeadersIV, + ciphertext: el.encryptedHeadersCiphertext, + keyEncoding: el.encryptedHeadersKeyEncoding as SecretKeyEncoding + }); + + credentials = { + url: el.url, + headers: JSON.parse(decryptedHeaders) + }; + } else { + credentials = { + url: el.url, + headers: [] + }; + } + + const encryptedCredentials = orgKmsService.encryptor({ + plainText: Buffer.from(JSON.stringify(credentials), "utf8") + }).cipherTextBlob; + + return { + id: el.id, + orgId: el.orgId, + url: el.url, + provider, + encryptedCredentials + }; + }) + ); + + for (let i = 0; i < updatedLogStreams.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.AuditLogStream) + .insert(updatedLogStreams.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + + await knex.schema.alterTable(TableName.AuditLogStream, (t) => { + t.binary("encryptedCredentials").notNullable().alter(); + }); + } +} + +// IMPORTANT: The down migration does not utilize the existing "url" and encrypted header columns +// because we're taking the latest data from the credentials column and re-encrypting it into relevant columns +// +// If this down migration was to fail, you can fall-back to the existing URL and encrypted header columns to retrieve +// data that was created prior to this migration + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.AuditLogStream)) { + const superAdminDAL = superAdminDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL); + const keyStore = inMemoryKeyStore(); + + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const logStreamsToRevert = await knex(TableName.AuditLogStream) + .select("id", "orgId", "encryptedCredentials") + .where("provider", "custom") + .whereNotNull("encryptedCredentials"); + + const updatedLogStreams = await Promise.all( + logStreamsToRevert.map(async (el) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(el.orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.Organization, + orgId: el.orgId + }, + knex + ); + orgEncryptionRingBuffer.push(el.orgId, orgKmsService); + } + + const decryptedCredentials = orgKmsService + .decryptor({ + cipherTextBlob: el.encryptedCredentials + }) + .toString(); + + const credentials: { url: string; headers: { key: string; value: string }[] } = + JSON.parse(decryptedCredentials); + + const originalUrl: string = credentials.url; + + const encryptedHeadersResult = crypto + .encryption() + .symmetric() + .encryptWithRootEncryptionKey(JSON.stringify(credentials.headers), envConfig); + + const encryptedHeadersAlgorithm: string = encryptedHeadersResult.algorithm; + const encryptedHeadersCiphertext: string = encryptedHeadersResult.ciphertext; + const encryptedHeadersIV: string = encryptedHeadersResult.iv; + const encryptedHeadersKeyEncoding: string = encryptedHeadersResult.encoding; + const encryptedHeadersTag: string = encryptedHeadersResult.tag; + + return { + id: el.id, + orgId: el.orgId, + encryptedCredentials: el.encryptedCredentials, + + url: originalUrl, + encryptedHeadersAlgorithm, + encryptedHeadersCiphertext, + encryptedHeadersIV, + encryptedHeadersKeyEncoding, + encryptedHeadersTag + }; + }) + ); + + for (let i = 0; i < updatedLogStreams.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.AuditLogStream) + .insert(updatedLogStreams.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + + await knex(TableName.AuditLogStream).whereNot("provider", "custom").orWhereNull("url").del(); + + await knex.schema.alterTable(TableName.AuditLogStream, (t) => { + t.string("url").notNullable().alter(); + + t.dropColumn("provider"); + t.dropColumn("encryptedCredentials"); + }); + } +} diff --git a/backend/src/db/schemas/audit-log-streams.ts b/backend/src/db/schemas/audit-log-streams.ts index 901dd8d27..a3f6bafba 100644 --- a/backend/src/db/schemas/audit-log-streams.ts +++ b/backend/src/db/schemas/audit-log-streams.ts @@ -5,11 +5,13 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const AuditLogStreamsSchema = z.object({ id: z.string().uuid(), - url: z.string(), + url: z.string().nullable().optional(), encryptedHeadersCiphertext: z.string().nullable().optional(), encryptedHeadersIV: z.string().nullable().optional(), encryptedHeadersTag: z.string().nullable().optional(), @@ -17,7 +19,9 @@ export const AuditLogStreamsSchema = z.object({ encryptedHeadersKeyEncoding: z.string().nullable().optional(), orgId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + provider: z.string().default("custom"), + encryptedCredentials: zodBuffer }); export type TAuditLogStreams = z.infer; diff --git a/backend/src/ee/routes/v1/audit-log-stream-router.ts b/backend/src/ee/routes/v1/audit-log-stream-router.ts deleted file mode 100644 index 17bd9e64b..000000000 --- a/backend/src/ee/routes/v1/audit-log-stream-router.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { z } from "zod"; - -import { AUDIT_LOG_STREAMS } from "@app/lib/api-docs"; -import { readLimit } from "@app/server/config/rateLimiter"; -import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedAuditLogStreamSchema } from "@app/server/routes/sanitizedSchemas"; -import { AuthMode } from "@app/services/auth/auth-type"; - -export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) => { - server.route({ - method: "POST", - url: "/", - config: { - rateLimit: readLimit - }, - schema: { - description: "Create an Audit Log Stream.", - security: [ - { - bearerAuth: [] - } - ], - body: z.object({ - url: z.string().min(1).describe(AUDIT_LOG_STREAMS.CREATE.url), - headers: z - .object({ - key: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.CREATE.headers.key), - value: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.CREATE.headers.value) - }) - .describe(AUDIT_LOG_STREAMS.CREATE.headers.desc) - .array() - .optional() - }), - response: { - 200: z.object({ - auditLogStream: SanitizedAuditLogStreamSchema - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const auditLogStream = await server.services.auditLogStream.create({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - url: req.body.url, - headers: req.body.headers - }); - - return { auditLogStream }; - } - }); - - server.route({ - method: "PATCH", - url: "/:id", - config: { - rateLimit: readLimit - }, - schema: { - description: "Update an Audit Log Stream by ID.", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - id: z.string().describe(AUDIT_LOG_STREAMS.UPDATE.id) - }), - body: z.object({ - url: z.string().optional().describe(AUDIT_LOG_STREAMS.UPDATE.url), - headers: z - .object({ - key: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.UPDATE.headers.key), - value: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.UPDATE.headers.value) - }) - .describe(AUDIT_LOG_STREAMS.UPDATE.headers.desc) - .array() - .optional() - }), - response: { - 200: z.object({ - auditLogStream: SanitizedAuditLogStreamSchema - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const auditLogStream = await server.services.auditLogStream.updateById({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - id: req.params.id, - url: req.body.url, - headers: req.body.headers - }); - - return { auditLogStream }; - } - }); - - server.route({ - method: "DELETE", - url: "/:id", - config: { - rateLimit: readLimit - }, - schema: { - description: "Delete an Audit Log Stream by ID.", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - id: z.string().describe(AUDIT_LOG_STREAMS.DELETE.id) - }), - response: { - 200: z.object({ - auditLogStream: SanitizedAuditLogStreamSchema - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const auditLogStream = await server.services.auditLogStream.deleteById({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - id: req.params.id - }); - - return { auditLogStream }; - } - }); - - server.route({ - method: "GET", - url: "/:id", - config: { - rateLimit: readLimit - }, - schema: { - description: "Get an Audit Log Stream by ID.", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - id: z.string().describe(AUDIT_LOG_STREAMS.GET_BY_ID.id) - }), - response: { - 200: z.object({ - auditLogStream: SanitizedAuditLogStreamSchema.extend({ - headers: z - .object({ - key: z.string(), - value: z.string() - }) - .array() - .optional() - }) - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const auditLogStream = await server.services.auditLogStream.getById({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - id: req.params.id - }); - - return { auditLogStream }; - } - }); - - server.route({ - method: "GET", - url: "/", - config: { - rateLimit: readLimit - }, - schema: { - description: "List Audit Log Streams.", - security: [ - { - bearerAuth: [] - } - ], - response: { - 200: z.object({ - auditLogStreams: SanitizedAuditLogStreamSchema.array() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const auditLogStreams = await server.services.auditLogStream.list({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod - }); - - return { auditLogStreams }; - } - }); -}; diff --git a/backend/src/ee/routes/v1/audit-log-stream-routers/audit-log-stream-endpoints.ts b/backend/src/ee/routes/v1/audit-log-stream-routers/audit-log-stream-endpoints.ts new file mode 100644 index 000000000..816fc62fb --- /dev/null +++ b/backend/src/ee/routes/v1/audit-log-stream-routers/audit-log-stream-endpoints.ts @@ -0,0 +1,142 @@ +import { z } from "zod"; + +import { LogProvider } from "@app/ee/services/audit-log-stream/audit-log-stream-enums"; +import { TAuditLogStream } from "@app/ee/services/audit-log-stream/audit-log-stream-types"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerAuditLogStreamEndpoints = ({ + server, + provider, + createSchema, + updateSchema, + sanitizedResponseSchema +}: { + server: FastifyZodProvider; + provider: LogProvider; + createSchema: z.ZodType<{ + credentials: T["credentials"]; + }>; + updateSchema: z.ZodType<{ + credentials: T["credentials"]; + }>; + sanitizedResponseSchema: z.ZodTypeAny; +}) => { + server.route({ + method: "GET", + url: "/:logStreamId", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + logStreamId: z.string().uuid() + }), + response: { + 200: z.object({ + auditLogStream: sanitizedResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { logStreamId } = req.params; + + const auditLogStream = await server.services.auditLogStream.getById(logStreamId, provider, req.permission); + + return { auditLogStream }; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + body: createSchema, + response: { + 200: z.object({ + auditLogStream: sanitizedResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { credentials } = req.body; + + const auditLogStream = await server.services.auditLogStream.create( + { + provider, + credentials + }, + req.permission + ); + + return { auditLogStream }; + } + }); + + server.route({ + method: "PATCH", + url: "/:logStreamId", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + logStreamId: z.string().uuid() + }), + body: updateSchema, + response: { + 200: z.object({ + auditLogStream: sanitizedResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { logStreamId } = req.params; + const { credentials } = req.body; + + const auditLogStream = await server.services.auditLogStream.updateById( + { + logStreamId, + provider, + credentials + }, + req.permission + ); + + return { auditLogStream }; + } + }); + + server.route({ + method: "DELETE", + url: "/:logStreamId", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + logStreamId: z.string().uuid() + }), + response: { + 200: z.object({ + auditLogStream: sanitizedResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { logStreamId } = req.params; + + const auditLogStream = await server.services.auditLogStream.deleteById(logStreamId, provider, req.permission); + + return { auditLogStream }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/audit-log-stream-routers/audit-log-stream-router.ts b/backend/src/ee/routes/v1/audit-log-stream-routers/audit-log-stream-router.ts new file mode 100644 index 000000000..bf10e48bf --- /dev/null +++ b/backend/src/ee/routes/v1/audit-log-stream-routers/audit-log-stream-router.ts @@ -0,0 +1,73 @@ +import { z } from "zod"; + +import { + CustomProviderListItemSchema, + SanitizedCustomProviderSchema +} from "@app/ee/services/audit-log-stream/custom/custom-provider-schemas"; +import { + DatadogProviderListItemSchema, + SanitizedDatadogProviderSchema +} from "@app/ee/services/audit-log-stream/datadog/datadog-provider-schemas"; +import { + SanitizedSplunkProviderSchema, + SplunkProviderListItemSchema +} from "@app/ee/services/audit-log-stream/splunk/splunk-provider-schemas"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const SanitizedAuditLogStreamSchema = z.union([ + SanitizedCustomProviderSchema, + SanitizedDatadogProviderSchema, + SanitizedSplunkProviderSchema +]); + +const ProviderOptionsSchema = z.discriminatedUnion("provider", [ + CustomProviderListItemSchema, + DatadogProviderListItemSchema, + SplunkProviderListItemSchema +]); + +export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/options", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + providerOptions: ProviderOptionsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: () => { + const providerOptions = server.services.auditLogStream.listProviderOptions(); + + return { providerOptions }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + auditLogStreams: SanitizedAuditLogStreamSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const auditLogStreams = await server.services.auditLogStream.list(req.permission); + + return { auditLogStreams }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/audit-log-stream-routers/index.ts b/backend/src/ee/routes/v1/audit-log-stream-routers/index.ts new file mode 100644 index 000000000..2f19e7522 --- /dev/null +++ b/backend/src/ee/routes/v1/audit-log-stream-routers/index.ts @@ -0,0 +1,48 @@ +import { LogProvider } from "@app/ee/services/audit-log-stream/audit-log-stream-enums"; +import { + CustomProviderSchema, + SanitizedCustomProviderSchema +} from "@app/ee/services/audit-log-stream/custom/custom-provider-schemas"; +import { + DatadogProviderSchema, + SanitizedDatadogProviderSchema +} from "@app/ee/services/audit-log-stream/datadog/datadog-provider-schemas"; +import { + SanitizedSplunkProviderSchema, + SplunkProviderSchema +} from "@app/ee/services/audit-log-stream/splunk/splunk-provider-schemas"; + +import { registerAuditLogStreamEndpoints } from "./audit-log-stream-endpoints"; + +export * from "./audit-log-stream-router"; + +export const AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP: Record Promise> = + { + [LogProvider.Custom]: async (server: FastifyZodProvider) => { + registerAuditLogStreamEndpoints({ + server, + provider: LogProvider.Custom, + sanitizedResponseSchema: SanitizedCustomProviderSchema, + createSchema: CustomProviderSchema, + updateSchema: CustomProviderSchema + }); + }, + [LogProvider.Datadog]: async (server: FastifyZodProvider) => { + registerAuditLogStreamEndpoints({ + server, + provider: LogProvider.Datadog, + sanitizedResponseSchema: SanitizedDatadogProviderSchema, + createSchema: DatadogProviderSchema, + updateSchema: DatadogProviderSchema + }); + }, + [LogProvider.Splunk]: async (server: FastifyZodProvider) => { + registerAuditLogStreamEndpoints({ + server, + provider: LogProvider.Splunk, + sanitizedResponseSchema: SanitizedSplunkProviderSchema, + createSchema: SplunkProviderSchema, + updateSchema: SplunkProviderSchema + }); + } + }; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index ab9503f58..35179c845 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -3,7 +3,7 @@ import { registerProjectTemplateRouter } from "@app/ee/routes/v1/project-templat import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router"; import { registerAccessApprovalRequestRouter } from "./access-approval-request-router"; import { registerAssumePrivilegeRouter } from "./assume-privilege-router"; -import { registerAuditLogStreamRouter } from "./audit-log-stream-router"; +import { AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP, registerAuditLogStreamRouter } from "./audit-log-stream-routers"; import { registerCaCrlRouter } from "./certificate-authority-crl-router"; import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerKubernetesDynamicSecretLeaseRouter } from "./dynamic-secret-lease-routers/kubernetes-lease-router"; @@ -114,7 +114,19 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { await server.register(registerSecretRouter, { prefix: "/secrets" }); await server.register(registerSecretVersionRouter, { prefix: "/secret" }); await server.register(registerGroupRouter, { prefix: "/groups" }); - await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" }); + + await server.register( + async (auditLogStreamRouter) => { + await auditLogStreamRouter.register(registerAuditLogStreamRouter); + + // Provider-specific endpoints + for await (const [provider, router] of Object.entries(AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP)) { + await auditLogStreamRouter.register(router, { prefix: `/${provider}` }); + } + }, + { prefix: "/audit-log-streams" } + ); + await server.register(registerUserAdditionalPrivilegeRouter, { prefix: "/user-project-additional-privilege" }); await server.register( async (privilegeRouter) => { diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-enums.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-enums.ts new file mode 100644 index 000000000..9c03dc2bd --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-enums.ts @@ -0,0 +1,5 @@ +export enum LogProvider { + Datadog = "datadog", + Splunk = "splunk", + Custom = "custom" +} diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-factory.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-factory.ts new file mode 100644 index 000000000..f70dc02a0 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-factory.ts @@ -0,0 +1,13 @@ +import { LogProvider } from "./audit-log-stream-enums"; +import { TAuditLogStreamCredentials, TLogStreamFactory } from "./audit-log-stream-types"; +import { CustomProviderFactory } from "./custom/custom-provider-factory"; +import { DatadogProviderFactory } from "./datadog/datadog-provider-factory"; +import { SplunkProviderFactory } from "./splunk/splunk-provider-factory"; + +type TLogStreamFactoryImplementation = TLogStreamFactory; + +export const LOG_STREAM_FACTORY_MAP: Record = { + [LogProvider.Datadog]: DatadogProviderFactory as TLogStreamFactoryImplementation, + [LogProvider.Splunk]: SplunkProviderFactory as TLogStreamFactoryImplementation, + [LogProvider.Custom]: CustomProviderFactory as TLogStreamFactoryImplementation +}; diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-fns.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-fns.ts index dc93f238e..bf6e4d47d 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-fns.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-fns.ts @@ -1,21 +1,70 @@ -export function providerSpecificPayload(url: string) { - const { hostname } = new URL(url); +import { TAuditLogStreams } from "@app/db/schemas"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; - const payload: Record = {}; +import { TAuditLogStream, TAuditLogStreamCredentials } from "./audit-log-stream-types"; +import { getCustomProviderListItem } from "./custom/custom-provider-fns"; +import { getDatadogProviderListItem } from "./datadog/datadog-provider-fns"; +import { getSplunkProviderListItem } from "./splunk/splunk-provider-fns"; - switch (hostname) { - case "http-intake.logs.datadoghq.com": - case "http-intake.logs.us3.datadoghq.com": - case "http-intake.logs.us5.datadoghq.com": - case "http-intake.logs.datadoghq.eu": - case "http-intake.logs.ap1.datadoghq.com": - case "http-intake.logs.ddog-gov.com": - payload.ddsource = "infisical"; - payload.service = "audit-logs"; - break; - default: - break; - } +export const listProviderOptions = () => { + return [getCustomProviderListItem(), getDatadogProviderListItem(), getSplunkProviderListItem()].sort((a, b) => + a.name.localeCompare(b.name) + ); +}; - return payload; -} +export const encryptLogStreamCredentials = async ({ + orgId, + credentials, + kmsService +}: { + orgId: string; + credentials: TAuditLogStreamCredentials; + kmsService: Pick; +}) => { + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }); + + const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({ + plainText: Buffer.from(JSON.stringify(credentials)) + }); + + return encryptedCredentialsBlob; +}; + +export const decryptLogStreamCredentials = async ({ + orgId, + encryptedCredentials, + kmsService +}: { + orgId: string; + encryptedCredentials: Buffer; + kmsService: Pick; +}) => { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }); + + const decryptedPlainTextBlob = decryptor({ + cipherTextBlob: encryptedCredentials + }); + + return JSON.parse(decryptedPlainTextBlob.toString()) as TAuditLogStreamCredentials; +}; + +export const decryptLogStream = async ( + logStream: TAuditLogStreams, + kmsService: Pick +) => { + return { + ...logStream, + credentials: await decryptLogStreamCredentials({ + encryptedCredentials: logStream.encryptedCredentials, + orgId: logStream.orgId, + kmsService + }) + } as TAuditLogStream; +}; diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts index 46d2782b3..773f32407 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts @@ -1,242 +1,215 @@ import { ForbiddenError } from "@casl/ability"; -import { RawAxiosRequestHeaders } from "axios"; +import { AxiosError } from "axios"; -import { SecretKeyEncoding } from "@app/db/schemas"; -import { getConfig } from "@app/lib/config/env"; -import { request } from "@app/lib/config/request"; -import { crypto } from "@app/lib/crypto/cryptography"; -import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; -import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { TAuditLogs } from "@app/db/schemas"; +import { + decryptLogStream, + decryptLogStreamCredentials, + encryptLogStreamCredentials, + listProviderOptions +} from "@app/ee/services/audit-log-stream/audit-log-stream-fns"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal"; -import { providerSpecificPayload } from "./audit-log-stream-fns"; -import { LogStreamHeaders, TAuditLogStreamServiceFactory } from "./audit-log-stream-types"; +import { LogProvider } from "./audit-log-stream-enums"; +import { LOG_STREAM_FACTORY_MAP } from "./audit-log-stream-factory"; +import { TAuditLogStream, TCreateAuditLogStreamDTO, TUpdateAuditLogStreamDTO } from "./audit-log-stream-types"; -type TAuditLogStreamServiceFactoryDep = { +export type TAuditLogStreamServiceFactoryDep = { auditLogStreamDAL: TAuditLogStreamDALFactory; permissionService: Pick; licenseService: Pick; + kmsService: Pick; }; +export type TAuditLogStreamServiceFactory = ReturnType; + export const auditLogStreamServiceFactory = ({ auditLogStreamDAL, permissionService, - licenseService -}: TAuditLogStreamServiceFactoryDep): TAuditLogStreamServiceFactory => { - const create: TAuditLogStreamServiceFactory["create"] = async ({ - url, - actor, - headers = [], - actorId, - actorOrgId, - actorAuthMethod - }) => { - if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" }); - - const plan = await licenseService.getPlan(actorOrgId); + licenseService, + kmsService +}: TAuditLogStreamServiceFactoryDep) => { + const create = async ({ provider, credentials }: TCreateAuditLogStreamDTO, actor: OrgServiceActor) => { + const plan = await licenseService.getPlan(actor.orgId); if (!plan.auditLogStreams) { throw new BadRequestError({ - message: "Failed to create audit log streams due to plan restriction. Upgrade plan to create group." + message: "Failed to create Audit Log Stream: Plan restriction. Upgrade plan to continue." }); } const { permission } = await permissionService.getOrgPermission( - actor, - actorId, - actorOrgId, - actorAuthMethod, - actorOrgId + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + actor.orgId ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); - const appCfg = getConfig(); - if (appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url); - - const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId }); + const totalStreams = await auditLogStreamDAL.find({ orgId: actor.orgId }); if (totalStreams.length >= plan.auditLogStreamLimit) { throw new BadRequestError({ - message: - "Failed to create audit log streams due to plan limit reached. Kindly contact Infisical to add more streams." + message: "Failed to create Audit Log Stream: Plan limit reached. Contact Infisical to increase quota." }); } - // testing connection first - const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; - if (headers.length) - headers.forEach(({ key, value }) => { - streamHeaders[key] = value; - }); + const factory = LOG_STREAM_FACTORY_MAP[provider](); + const validatedCredentials = await factory.validateCredentials({ credentials }); - await request - .post( - url, - { ...providerSpecificPayload(url), ping: "ok" }, - { - headers: streamHeaders, - // request timeout - timeout: AUDIT_LOG_STREAM_TIMEOUT, - // connection timeout - signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) - } - ) - .catch((err) => { - throw new BadRequestError({ message: `Failed to connect with upstream source: ${(err as Error)?.message}` }); - }); + const encryptedCredentials = await encryptLogStreamCredentials({ + credentials: validatedCredentials, + orgId: actor.orgId, + kmsService + }); - const encryptedHeaders = headers - ? crypto.encryption().symmetric().encryptWithRootEncryptionKey(JSON.stringify(headers)) - : undefined; const logStream = await auditLogStreamDAL.create({ - orgId: actorOrgId, - url, - ...(encryptedHeaders - ? { - encryptedHeadersCiphertext: encryptedHeaders.ciphertext, - encryptedHeadersIV: encryptedHeaders.iv, - encryptedHeadersTag: encryptedHeaders.tag, - encryptedHeadersAlgorithm: encryptedHeaders.algorithm, - encryptedHeadersKeyEncoding: encryptedHeaders.encoding - } - : {}) + orgId: actor.orgId, + provider, + encryptedCredentials }); - return logStream; + + return { ...logStream, credentials: validatedCredentials } as TAuditLogStream; }; - const updateById: TAuditLogStreamServiceFactory["updateById"] = async ({ - id, - url, - actor, - headers = [], - actorId, - actorOrgId, - actorAuthMethod - }) => { - if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" }); - - const plan = await licenseService.getPlan(actorOrgId); - if (!plan.auditLogStreams) + const updateById = async ( + { logStreamId, provider, credentials }: TUpdateAuditLogStreamDTO, + actor: OrgServiceActor + ) => { + const plan = await licenseService.getPlan(actor.orgId); + if (!plan.auditLogStreams) { throw new BadRequestError({ - message: "Failed to update audit log streams due to plan restriction. Upgrade plan to create group." + message: "Failed to update Audit Log Stream: Plan restriction. Upgrade plan to continue." }); + } - const logStream = await auditLogStreamDAL.findById(id); - if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` }); + const logStream = await auditLogStreamDAL.findById(logStreamId); + if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` }); + + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + logStream.orgId + ); - const { orgId } = logStream; - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); - const appCfg = getConfig(); - if (url && appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url); - // testing connection first - const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; - if (headers.length) - headers.forEach(({ key, value }) => { - streamHeaders[key] = value; - }); + const factory = LOG_STREAM_FACTORY_MAP[provider](); + const validatedCredentials = await factory.validateCredentials({ credentials }); - await request - .post( - url || logStream.url, - { ...providerSpecificPayload(url || logStream.url), ping: "ok" }, - { - headers: streamHeaders, - // request timeout - timeout: AUDIT_LOG_STREAM_TIMEOUT, - // connection timeout - signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) - } - ) - .catch((err) => { - throw new Error(`Failed to connect with the source ${(err as Error)?.message}`); - }); - - const encryptedHeaders = headers - ? crypto.encryption().symmetric().encryptWithRootEncryptionKey(JSON.stringify(headers)) - : undefined; - const updatedLogStream = await auditLogStreamDAL.updateById(id, { - url, - ...(encryptedHeaders - ? { - encryptedHeadersCiphertext: encryptedHeaders.ciphertext, - encryptedHeadersIV: encryptedHeaders.iv, - encryptedHeadersTag: encryptedHeaders.tag, - encryptedHeadersAlgorithm: encryptedHeaders.algorithm, - encryptedHeadersKeyEncoding: encryptedHeaders.encoding - } - : {}) + const encryptedCredentials = await encryptLogStreamCredentials({ + credentials: validatedCredentials, + orgId: actor.orgId, + kmsService }); - return updatedLogStream; + + const updatedLogStream = await auditLogStreamDAL.updateById(logStreamId, { + encryptedCredentials + }); + + return { ...updatedLogStream, credentials: validatedCredentials } as TAuditLogStream; }; - const deleteById: TAuditLogStreamServiceFactory["deleteById"] = async ({ - id, - actor, - actorId, - actorOrgId, - actorAuthMethod - }) => { - if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" }); + const deleteById = async (logStreamId: string, provider: LogProvider, actor: OrgServiceActor) => { + const logStream = await auditLogStreamDAL.findById(logStreamId); + if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` }); - const logStream = await auditLogStreamDAL.findById(id); - if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` }); + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + logStream.orgId + ); - const { orgId } = logStream; - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); - const deletedLogStream = await auditLogStreamDAL.deleteById(id); - return deletedLogStream; + if (logStream.provider !== provider) { + throw new BadRequestError({ + message: `Audit Log Stream with ID '${logStreamId}' is not for provider '${provider}'` + }); + } + + const deletedLogStream = await auditLogStreamDAL.deleteById(logStreamId); + + return decryptLogStream(deletedLogStream, kmsService); }; - const getById: TAuditLogStreamServiceFactory["getById"] = async ({ - id, - actor, - actorId, - actorOrgId, - actorAuthMethod - }) => { - const logStream = await auditLogStreamDAL.findById(id); - if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${id}' not found` }); + const getById = async (logStreamId: string, provider: LogProvider, actor: OrgServiceActor) => { + const logStream = await auditLogStreamDAL.findById(logStreamId); - const { orgId } = logStream; - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); + if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${logStreamId}' not found` }); - const headers = - logStream?.encryptedHeadersCiphertext && logStream?.encryptedHeadersIV && logStream?.encryptedHeadersTag - ? (JSON.parse( - crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - tag: logStream.encryptedHeadersTag, - iv: logStream.encryptedHeadersIV, - ciphertext: logStream.encryptedHeadersCiphertext, - keyEncoding: logStream.encryptedHeadersKeyEncoding as SecretKeyEncoding - }) - ) as LogStreamHeaders[]) - : undefined; - - return { ...logStream, headers }; - }; - - const list: TAuditLogStreamServiceFactory["list"] = async ({ actor, actorId, actorOrgId, actorAuthMethod }) => { const { permission } = await permissionService.getOrgPermission( - actor, - actorId, - actorOrgId, - actorAuthMethod, - actorOrgId + actor.type, + actor.id, + logStream.orgId, + actor.authMethod, + actor.orgId ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); - const logStreams = await auditLogStreamDAL.find({ orgId: actorOrgId }); - return logStreams; + if (logStream.provider !== provider) { + throw new BadRequestError({ + message: `Audit Log Stream with ID '${logStreamId}' is not for provider '${provider}'` + }); + } + + return decryptLogStream(logStream, kmsService); + }; + + const list = async (actor: OrgServiceActor) => { + const { permission } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + actor.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); + + const logStreams = await auditLogStreamDAL.find({ orgId: actor.orgId }); + + return Promise.all(logStreams.map((stream) => decryptLogStream(stream, kmsService))); + }; + + const streamLog = async (orgId: string, auditLog: TAuditLogs) => { + const logStreams = await auditLogStreamDAL.find({ orgId }); + await Promise.allSettled( + logStreams.map(async ({ provider, encryptedCredentials }) => { + const credentials = await decryptLogStreamCredentials({ + encryptedCredentials, + orgId, + kmsService + }); + + const factory = LOG_STREAM_FACTORY_MAP[provider as LogProvider](); + + try { + await factory.streamLog({ + credentials, + auditLog + }); + } catch (error) { + logger.error( + error, + `Failed to stream audit log [auditLogId=${auditLog.id}] [provider=${provider}] [orgId=${orgId}]${error instanceof AxiosError ? `: ${error.message}` : ""}` + ); + throw error; + } + }) + ); }; return { @@ -244,6 +217,8 @@ export const auditLogStreamServiceFactory = ({ updateById, deleteById, getById, - list + list, + listProviderOptions, + streamLog }; }; diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts index 4c4a5609e..d0f78f632 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts @@ -1,48 +1,38 @@ -import { TAuditLogStreams } from "@app/db/schemas"; -import { TOrgPermission } from "@app/lib/types"; +import { TAuditLogs } from "@app/db/schemas"; -export type LogStreamHeaders = { - key: string; - value: string; +import { LogProvider } from "./audit-log-stream-enums"; +import { TCustomProvider, TCustomProviderCredentials } from "./custom/custom-provider-types"; +import { TDatadogProvider, TDatadogProviderCredentials } from "./datadog/datadog-provider-types"; +import { TSplunkProvider, TSplunkProviderCredentials } from "./splunk/splunk-provider-types"; + +export type TAuditLogStream = TDatadogProvider | TSplunkProvider | TCustomProvider; + +export type TAuditLogStreamCredentials = + | TDatadogProviderCredentials + | TSplunkProviderCredentials + | TCustomProviderCredentials; + +export type TCreateAuditLogStreamDTO = { + provider: LogProvider; + credentials: TAuditLogStreamCredentials; }; -export type TCreateAuditLogStreamDTO = Omit & { - url: string; - headers?: LogStreamHeaders[]; +export type TUpdateAuditLogStreamDTO = { + logStreamId: string; + provider: LogProvider; + credentials: TAuditLogStreamCredentials; }; -export type TUpdateAuditLogStreamDTO = Omit & { - id: string; - url?: string; - headers?: LogStreamHeaders[]; -}; +export type TLogStreamFactoryValidateCredentials = (input: { + credentials: C; +}) => Promise; -export type TDeleteAuditLogStreamDTO = Omit & { - id: string; -}; +export type TLogStreamFactoryStreamLog = (input: { + credentials: C; + auditLog: TAuditLogs; +}) => Promise; -export type TListAuditLogStreamDTO = Omit; - -export type TGetDetailsAuditLogStreamDTO = Omit & { - id: string; -}; - -export type TAuditLogStreamServiceFactory = { - create: (arg: TCreateAuditLogStreamDTO) => Promise; - updateById: (arg: TUpdateAuditLogStreamDTO) => Promise; - deleteById: (arg: TDeleteAuditLogStreamDTO) => Promise; - getById: (arg: TGetDetailsAuditLogStreamDTO) => Promise<{ - headers: LogStreamHeaders[] | undefined; - orgId: string; - url: string; - id: string; - createdAt: Date; - updatedAt: Date; - encryptedHeadersCiphertext?: string | null | undefined; - encryptedHeadersIV?: string | null | undefined; - encryptedHeadersTag?: string | null | undefined; - encryptedHeadersAlgorithm?: string | null | undefined; - encryptedHeadersKeyEncoding?: string | null | undefined; - }>; - list: (arg: TListAuditLogStreamDTO) => Promise; +export type TLogStreamFactory = () => { + validateCredentials: TLogStreamFactoryValidateCredentials; + streamLog: TLogStreamFactoryStreamLog; }; diff --git a/backend/src/ee/services/audit-log-stream/custom/custom-provider-factory.ts b/backend/src/ee/services/audit-log-stream/custom/custom-provider-factory.ts new file mode 100644 index 000000000..6e397638c --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/custom/custom-provider-factory.ts @@ -0,0 +1,67 @@ +import { RawAxiosRequestHeaders } from "axios"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; + +import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue"; +import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types"; +import { TCustomProviderCredentials } from "./custom-provider-types"; + +export const CustomProviderFactory = () => { + const validateCredentials: TLogStreamFactoryValidateCredentials = async ({ + credentials + }) => { + const { url, headers } = credentials; + + await blockLocalAndPrivateIpAddresses(url); + + const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; + if (headers.length) { + headers.forEach(({ key, value }) => { + streamHeaders[key] = value; + }); + } + + await request + .post( + url, + { ping: "ok" }, + { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + } + ) + .catch((err) => { + throw new BadRequestError({ message: `Failed to connect with upstream source: ${(err as Error)?.message}` }); + }); + + return credentials; + }; + + const streamLog: TLogStreamFactoryStreamLog = async ({ credentials, auditLog }) => { + const { url, headers } = credentials; + + await blockLocalAndPrivateIpAddresses(url); + + const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; + + if (headers.length) { + headers.forEach(({ key, value }) => { + streamHeaders[key] = value; + }); + } + + await request.post(url, auditLog, { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + }); + }; + + return { + validateCredentials, + streamLog + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/custom/custom-provider-fns.ts b/backend/src/ee/services/audit-log-stream/custom/custom-provider-fns.ts new file mode 100644 index 000000000..27b8bbf72 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/custom/custom-provider-fns.ts @@ -0,0 +1,8 @@ +import { LogProvider } from "../audit-log-stream-enums"; + +export const getCustomProviderListItem = () => { + return { + name: "Custom" as const, + provider: LogProvider.Custom as const + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/custom/custom-provider-schemas.ts b/backend/src/ee/services/audit-log-stream/custom/custom-provider-schemas.ts new file mode 100644 index 000000000..5d375c42f --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/custom/custom-provider-schemas.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; + +import { LogProvider } from "../audit-log-stream-enums"; + +export const CustomProviderCredentialsSchema = z.object({ + url: z.string().url().trim().min(1).max(255), + headers: z + .object({ + key: z.string().min(1), + value: z.string().min(1) + }) + .array() +}); + +export const CustomProviderSchema = z.object({ + provider: z.literal(LogProvider.Custom), + credentials: CustomProviderCredentialsSchema +}); + +export const SanitizedCustomProviderSchema = z.object({ + provider: z.literal(LogProvider.Custom), + credentials: z.object({ + url: CustomProviderCredentialsSchema.shape.url, + // Only return header keys + headers: CustomProviderCredentialsSchema.shape.headers.element.pick({ key: true }).array() + }) +}); + +export const CustomProviderListItemSchema = z.object({ + name: z.literal("Custom"), + provider: z.literal(LogProvider.Custom) +}); diff --git a/backend/src/ee/services/audit-log-stream/custom/custom-provider-types.ts b/backend/src/ee/services/audit-log-stream/custom/custom-provider-types.ts new file mode 100644 index 000000000..9b2de8347 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/custom/custom-provider-types.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +import { CustomProviderCredentialsSchema, CustomProviderSchema } from "./custom-provider-schemas"; + +export type TCustomProvider = z.infer; + +export type TCustomProviderCredentials = z.infer; diff --git a/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-factory.ts b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-factory.ts new file mode 100644 index 000000000..f4b8738fa --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-factory.ts @@ -0,0 +1,60 @@ +import { RawAxiosRequestHeaders } from "axios"; + +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; + +import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue"; +import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types"; +import { TDatadogProviderCredentials } from "./datadog-provider-types"; + +export const DatadogProviderFactory = () => { + const validateCredentials: TLogStreamFactoryValidateCredentials = async ({ + credentials + }) => { + const { url, token } = credentials; + + await blockLocalAndPrivateIpAddresses(url); + + const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json", "DD-API-KEY": token }; + + await request + .post( + url, + { ping: "ok" }, + { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + } + ) + .catch((err) => { + throw new BadRequestError({ message: `Failed to connect with Datadog: ${(err as Error)?.message}` }); + }); + + return credentials; + }; + + const streamLog: TLogStreamFactoryStreamLog = async ({ credentials, auditLog }) => { + const { url, token } = credentials; + + await blockLocalAndPrivateIpAddresses(url); + + const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json", "DD-API-KEY": token }; + + await request.post( + url, + { ...auditLog, ddsource: "infisical", service: "audit-logs" }, + { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + } + ); + }; + + return { + validateCredentials, + streamLog + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-fns.ts b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-fns.ts new file mode 100644 index 000000000..ec68fdb39 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-fns.ts @@ -0,0 +1,8 @@ +import { LogProvider } from "../audit-log-stream-enums"; + +export const getDatadogProviderListItem = () => { + return { + name: "Datadog" as const, + provider: LogProvider.Datadog as const + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-schemas.ts b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-schemas.ts new file mode 100644 index 000000000..7adae1ae6 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-schemas.ts @@ -0,0 +1,29 @@ +import RE2 from "re2"; +import { z } from "zod"; + +import { LogProvider } from "../audit-log-stream-enums"; + +export const DatadogProviderCredentialsSchema = z.object({ + url: z.string().url().trim().min(1).max(255), + token: z + .string() + .trim() + .refine((val) => new RE2(/^[a-fA-F0-9]{32}$/).test(val), "Invalid Datadog API key format") +}); + +export const DatadogProviderSchema = z.object({ + provider: z.literal(LogProvider.Datadog), + credentials: DatadogProviderCredentialsSchema +}); + +export const SanitizedDatadogProviderSchema = z.object({ + provider: z.literal(LogProvider.Datadog), + credentials: DatadogProviderCredentialsSchema.pick({ + url: true + }) +}); + +export const DatadogProviderListItemSchema = z.object({ + name: z.literal("Datadog"), + provider: z.literal(LogProvider.Datadog) +}); diff --git a/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-types.ts b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-types.ts new file mode 100644 index 000000000..e7f6a1f28 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-types.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +import { DatadogProviderCredentialsSchema, DatadogProviderSchema } from "./datadog-provider-schemas"; + +export type TDatadogProvider = z.infer; + +export type TDatadogProviderCredentials = z.infer; diff --git a/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-factory.ts b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-factory.ts new file mode 100644 index 000000000..5481c44eb --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-factory.ts @@ -0,0 +1,84 @@ +import { RawAxiosRequestHeaders } from "axios"; + +import { getConfig } from "@app/lib/config/env"; +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; + +import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue"; +import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types"; +import { TSplunkProviderCredentials } from "./splunk-provider-types"; + +function createPayload(event: Record) { + const appCfg = getConfig(); + + return { + time: Math.floor(Date.now() / 1000), + host: new URL(appCfg.SITE_URL || "http://infisical").host, + source: "infisical", + sourcetype: "_json", + event + }; +} + +async function createSplunkUrl(hostname: string) { + let parsedHostname: string; + try { + parsedHostname = new URL(`https://${hostname}`).hostname; + } catch (error) { + throw new BadRequestError({ message: `Invalid Splunk hostname provided: ${(error as Error).message}` }); + } + + await blockLocalAndPrivateIpAddresses(`https://${hostname}`); + + return `https://${parsedHostname}:8088/services/collector/event`; +} + +export const SplunkProviderFactory = () => { + const validateCredentials: TLogStreamFactoryValidateCredentials = async ({ + credentials + }) => { + const { hostname, token } = credentials; + + const url = await createSplunkUrl(hostname); + + const streamHeaders: RawAxiosRequestHeaders = { + "Content-Type": "application/json", + Authorization: `Splunk ${token}` + }; + + await request + .post(url, createPayload({ ping: "ok" }), { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + }) + .catch((err) => { + throw new BadRequestError({ message: `Failed to connect with Splunk: ${(err as Error)?.message}` }); + }); + + return credentials; + }; + + const streamLog: TLogStreamFactoryStreamLog = async ({ credentials, auditLog }) => { + const { hostname, token } = credentials; + + const url = await createSplunkUrl(hostname); + + const streamHeaders: RawAxiosRequestHeaders = { + "Content-Type": "application/json", + Authorization: `Splunk ${token}` + }; + + await request.post(url, createPayload(auditLog), { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + }); + }; + + return { + validateCredentials, + streamLog + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-fns.ts b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-fns.ts new file mode 100644 index 000000000..e2ea2e316 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-fns.ts @@ -0,0 +1,8 @@ +import { LogProvider } from "../audit-log-stream-enums"; + +export const getSplunkProviderListItem = () => { + return { + name: "Splunk" as const, + provider: LogProvider.Splunk as const + }; +}; diff --git a/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-schemas.ts b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-schemas.ts new file mode 100644 index 000000000..b03524951 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-schemas.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +import { LogProvider } from "../audit-log-stream-enums"; + +export const SplunkProviderCredentialsSchema = z.object({ + hostname: z.string().url().trim().min(1).max(255), + token: z.string().uuid().trim().min(1) +}); + +export const SplunkProviderSchema = z.object({ + provider: z.literal(LogProvider.Splunk), + credentials: SplunkProviderCredentialsSchema +}); + +export const SanitizedSplunkProviderSchema = z.object({ + provider: z.literal(LogProvider.Splunk), + credentials: SplunkProviderCredentialsSchema.pick({ + hostname: true + }) +}); + +export const SplunkProviderListItemSchema = z.object({ + name: z.literal("Splunk"), + provider: z.literal(LogProvider.Splunk) +}); diff --git a/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-types.ts b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-types.ts new file mode 100644 index 000000000..11f7c8fcb --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-types.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +import { SplunkProviderCredentialsSchema, SplunkProviderSchema } from "./splunk-provider-schemas"; + +export type TSplunkProvider = z.infer; + +export type TSplunkProviderCredentials = z.infer; diff --git a/backend/src/ee/services/audit-log/audit-log-queue.ts b/backend/src/ee/services/audit-log/audit-log-queue.ts index 0914b8f6b..6b286c2ec 100644 --- a/backend/src/ee/services/audit-log/audit-log-queue.ts +++ b/backend/src/ee/services/audit-log/audit-log-queue.ts @@ -1,22 +1,14 @@ -import { AxiosError, RawAxiosRequestHeaders } from "axios"; - -import { SecretKeyEncoding } from "@app/db/schemas"; -import { request } from "@app/lib/config/request"; -import { crypto } from "@app/lib/crypto/cryptography"; -import { logger } from "@app/lib/logger"; +import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { TProjectDALFactory } from "@app/services/project/project-dal"; -import { TAuditLogStreamDALFactory } from "../audit-log-stream/audit-log-stream-dal"; -import { providerSpecificPayload } from "../audit-log-stream/audit-log-stream-fns"; -import { LogStreamHeaders } from "../audit-log-stream/audit-log-stream-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { TAuditLogDALFactory } from "./audit-log-dal"; import { TCreateAuditLogDTO } from "./audit-log-types"; type TAuditLogQueueServiceFactoryDep = { auditLogDAL: TAuditLogDALFactory; - auditLogStreamDAL: Pick; + auditLogStreamService: Pick; queueService: TQueueServiceFactory; projectDAL: Pick; licenseService: Pick; @@ -35,7 +27,7 @@ export const auditLogQueueServiceFactory = async ({ queueService, projectDAL, licenseService, - auditLogStreamDAL + auditLogStreamService }: TAuditLogQueueServiceFactoryDep): Promise => { const pushToLog = async (data: TCreateAuditLogDTO) => { await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, { @@ -86,60 +78,7 @@ export const auditLogQueueServiceFactory = async ({ userAgentType }); - const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : []; - await Promise.allSettled( - logStreams.map( - async ({ - url, - encryptedHeadersTag, - encryptedHeadersIV, - encryptedHeadersKeyEncoding, - encryptedHeadersCiphertext - }) => { - const streamHeaders = - encryptedHeadersIV && encryptedHeadersCiphertext && encryptedHeadersTag - ? (JSON.parse( - crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - keyEncoding: encryptedHeadersKeyEncoding as SecretKeyEncoding, - iv: encryptedHeadersIV, - tag: encryptedHeadersTag, - ciphertext: encryptedHeadersCiphertext - }) - ) as LogStreamHeaders[]) - : []; - - const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; - - if (streamHeaders.length) - streamHeaders.forEach(({ key, value }) => { - headers[key] = value; - }); - - try { - const response = await request.post( - url, - { ...providerSpecificPayload(url), ...auditLog }, - { - headers, - // request timeout - timeout: AUDIT_LOG_STREAM_TIMEOUT, - // connection timeout - signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) - } - ); - return response; - } catch (error) { - logger.error( - `Failed to stream audit log [url=${url}] for org [orgId=${orgId}] [error=${(error as AxiosError).message}]` - ); - return error; - } - } - ) - ); + await auditLogStreamService.streamLog(orgId, auditLog); } }); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 3773048b0..7c6f47b60 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -555,20 +555,22 @@ export const registerRoutes = async ( permissionService }); + const auditLogStreamService = auditLogStreamServiceFactory({ + licenseService, + permissionService, + auditLogStreamDAL, + kmsService + }); + const auditLogQueue = await auditLogQueueServiceFactory({ auditLogDAL, queueService, projectDAL, licenseService, - auditLogStreamDAL + auditLogStreamService }); const auditLogService = auditLogServiceFactory({ auditLogDAL, permissionService, auditLogQueue }); - const auditLogStreamService = auditLogStreamServiceFactory({ - licenseService, - permissionService, - auditLogStreamDAL - }); const secretApprovalPolicyService = secretApprovalPolicyServiceFactory({ projectEnvDAL, secretApprovalPolicyApproverDAL: sapApproverDAL, diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 8af4baa8b..344224008 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -246,13 +246,6 @@ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({ metadata: ResourceMetadataSchema.optional() }); -export const SanitizedAuditLogStreamSchema = z.object({ - id: z.string(), - url: z.string(), - createdAt: z.date(), - updatedAt: z.date() -}); - export const SanitizedProjectSchema = ProjectsSchema.pick({ id: true, name: true, From d534e82646a22e9356852b8df98cdaceaab9f1be Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 4 Sep 2025 22:14:11 -0400 Subject: [PATCH 03/10] feat(audit-log-stream): frontend rework + some backend changes --- .../v1/audit-log-stream-routers/index.ts | 25 +- .../audit-log-stream/audit-log-stream-fns.ts | 2 +- .../audit-log-stream-schemas.ts | 14 + .../custom/custom-provider-schemas.ts | 17 +- .../datadog/datadog-provider-factory.ts | 43 +-- .../datadog/datadog-provider-schemas.ts | 17 +- .../splunk/splunk-provider-schemas.ts | 44 ++- .../public/images/integrations/Datadog.png | Bin 0 -> 89071 bytes .../public/images/integrations/Splunk.png | Bin 0 -> 46489 bytes frontend/src/helpers/auditLogStreams.ts | 29 ++ .../src/hooks/api/auditLogStreams/enums.ts | 5 + .../src/hooks/api/auditLogStreams/index.tsx | 8 +- .../hooks/api/auditLogStreams/mutations.tsx | 52 +-- .../src/hooks/api/auditLogStreams/queries.tsx | 105 ++++-- .../src/hooks/api/auditLogStreams/types.ts | 28 -- .../hooks/api/auditLogStreams/types/index.ts | 25 ++ .../auditLogStreams/types/provider-options.ts | 11 + .../types/providers/custom-provider.ts | 10 + .../types/providers/datadog-provider.ts | 10 + .../types/providers/root-provider.ts | 6 + .../types/providers/splunk-provider.ts | 10 + .../AuditLogBanner/AuditLogBanner.tsx | 6 +- .../AuditLogStreamTab/AuditLogStreamForm.tsx | 207 ------------ .../AuditLogStreamForm/AuditLogStreamForm.tsx | 125 +++++++ .../CustomProviderAuditLogStreamForm.tsx | 154 +++++++++ .../DatadogProviderAuditLogStreamForm.tsx | 132 ++++++++ .../SplunkProviderAuditLogStreamForm.tsx | 120 +++++++ .../AuditLogStreamTab/AuditLogStreamTab.tsx | 167 +--------- .../components/AddAuditLogStreamModal.tsx | 69 ++++ .../components/AuditLogStreamHeader.tsx | 54 ++++ .../components/AuditLogStreamRow.tsx | 111 +++++++ .../components/AuditLogStreamTable.tsx | 304 ++++++++++++++++++ .../components/DeleteAuditLogStreamModal.tsx | 54 ++++ .../EditAuditLogStreamCredentialsModal.tsx | 34 ++ .../components/LogStreamProviderSelect.tsx | 155 +++++++++ .../AuditLogStreamTab/components/index.tsx | 1 + 36 files changed, 1664 insertions(+), 490 deletions(-) create mode 100644 backend/src/ee/services/audit-log-stream/audit-log-stream-schemas.ts create mode 100644 frontend/public/images/integrations/Datadog.png create mode 100644 frontend/public/images/integrations/Splunk.png create mode 100644 frontend/src/helpers/auditLogStreams.ts create mode 100644 frontend/src/hooks/api/auditLogStreams/enums.ts delete mode 100644 frontend/src/hooks/api/auditLogStreams/types.ts create mode 100644 frontend/src/hooks/api/auditLogStreams/types/index.ts create mode 100644 frontend/src/hooks/api/auditLogStreams/types/provider-options.ts create mode 100644 frontend/src/hooks/api/auditLogStreams/types/providers/custom-provider.ts create mode 100644 frontend/src/hooks/api/auditLogStreams/types/providers/datadog-provider.ts create mode 100644 frontend/src/hooks/api/auditLogStreams/types/providers/root-provider.ts create mode 100644 frontend/src/hooks/api/auditLogStreams/types/providers/splunk-provider.ts delete mode 100644 frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm.tsx create mode 100644 frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/AuditLogStreamForm.tsx create mode 100644 frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx create mode 100644 frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/DatadogProviderAuditLogStreamForm.tsx create mode 100644 frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/SplunkProviderAuditLogStreamForm.tsx create mode 100644 frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AddAuditLogStreamModal.tsx create mode 100644 frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamHeader.tsx create mode 100644 frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamRow.tsx create mode 100644 frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamTable.tsx create mode 100644 frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/DeleteAuditLogStreamModal.tsx create mode 100644 frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/EditAuditLogStreamCredentialsModal.tsx create mode 100644 frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/LogStreamProviderSelect.tsx create mode 100644 frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/index.tsx diff --git a/backend/src/ee/routes/v1/audit-log-stream-routers/index.ts b/backend/src/ee/routes/v1/audit-log-stream-routers/index.ts index 2f19e7522..ea3d48b30 100644 --- a/backend/src/ee/routes/v1/audit-log-stream-routers/index.ts +++ b/backend/src/ee/routes/v1/audit-log-stream-routers/index.ts @@ -1,15 +1,18 @@ import { LogProvider } from "@app/ee/services/audit-log-stream/audit-log-stream-enums"; import { - CustomProviderSchema, - SanitizedCustomProviderSchema + CreateCustomProviderLogStreamSchema, + SanitizedCustomProviderSchema, + UpdateCustomProviderLogStreamSchema } from "@app/ee/services/audit-log-stream/custom/custom-provider-schemas"; import { - DatadogProviderSchema, - SanitizedDatadogProviderSchema + CreateDatadogProviderLogStreamSchema, + SanitizedDatadogProviderSchema, + UpdateDatadogProviderLogStreamSchema } from "@app/ee/services/audit-log-stream/datadog/datadog-provider-schemas"; import { + CreateSplunkProviderLogStreamSchema, SanitizedSplunkProviderSchema, - SplunkProviderSchema + UpdateSplunkProviderLogStreamSchema } from "@app/ee/services/audit-log-stream/splunk/splunk-provider-schemas"; import { registerAuditLogStreamEndpoints } from "./audit-log-stream-endpoints"; @@ -23,8 +26,8 @@ export const AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP: Record { @@ -32,8 +35,8 @@ export const AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP: Record { @@ -41,8 +44,8 @@ export const AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP: Record { - return [getCustomProviderListItem(), getDatadogProviderListItem(), getSplunkProviderListItem()].sort((a, b) => + return [getDatadogProviderListItem(), getSplunkProviderListItem(), getCustomProviderListItem()].sort((a, b) => a.name.localeCompare(b.name) ); }; diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-schemas.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-schemas.ts new file mode 100644 index 000000000..4fba79107 --- /dev/null +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-schemas.ts @@ -0,0 +1,14 @@ +import { AuditLogStreamsSchema } from "@app/db/schemas"; + +export const BaseProviderSchema = AuditLogStreamsSchema.omit({ + encryptedCredentials: true, + provider: true, + + // Old "archived" values + encryptedHeadersAlgorithm: true, + encryptedHeadersCiphertext: true, + encryptedHeadersIV: true, + encryptedHeadersKeyEncoding: true, + encryptedHeadersTag: true, + url: true +}); diff --git a/backend/src/ee/services/audit-log-stream/custom/custom-provider-schemas.ts b/backend/src/ee/services/audit-log-stream/custom/custom-provider-schemas.ts index 5d375c42f..032f81be8 100644 --- a/backend/src/ee/services/audit-log-stream/custom/custom-provider-schemas.ts +++ b/backend/src/ee/services/audit-log-stream/custom/custom-provider-schemas.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { LogProvider } from "../audit-log-stream-enums"; +import { BaseProviderSchema } from "../audit-log-stream-schemas"; export const CustomProviderCredentialsSchema = z.object({ url: z.string().url().trim().min(1).max(255), @@ -12,13 +13,13 @@ export const CustomProviderCredentialsSchema = z.object({ .array() }); -export const CustomProviderSchema = z.object({ - provider: z.literal(LogProvider.Custom), +const BaseCustomProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Custom) }); + +export const CustomProviderSchema = BaseCustomProviderSchema.extend({ credentials: CustomProviderCredentialsSchema }); -export const SanitizedCustomProviderSchema = z.object({ - provider: z.literal(LogProvider.Custom), +export const SanitizedCustomProviderSchema = BaseCustomProviderSchema.extend({ credentials: z.object({ url: CustomProviderCredentialsSchema.shape.url, // Only return header keys @@ -30,3 +31,11 @@ export const CustomProviderListItemSchema = z.object({ name: z.literal("Custom"), provider: z.literal(LogProvider.Custom) }); + +export const CreateCustomProviderLogStreamSchema = z.object({ + credentials: CustomProviderCredentialsSchema +}); + +export const UpdateCustomProviderLogStreamSchema = z.object({ + credentials: CustomProviderCredentialsSchema +}); diff --git a/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-factory.ts b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-factory.ts index f4b8738fa..ec55784da 100644 --- a/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-factory.ts +++ b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-factory.ts @@ -1,5 +1,6 @@ import { RawAxiosRequestHeaders } from "axios"; +import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { BadRequestError } from "@app/lib/errors"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; @@ -8,6 +9,20 @@ import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue"; import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types"; import { TDatadogProviderCredentials } from "./datadog-provider-types"; +function createPayload(event: Record) { + const appCfg = getConfig(); + + const ddtags = [`env:${appCfg.NODE_ENV || "unknown"}`].join(","); + + return { + ...event, + hostname: new URL(appCfg.SITE_URL || "http://infisical").hostname, + ddsource: "infisical", + service: "infisical", + ddtags + }; +} + export const DatadogProviderFactory = () => { const validateCredentials: TLogStreamFactoryValidateCredentials = async ({ credentials @@ -19,15 +34,11 @@ export const DatadogProviderFactory = () => { const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json", "DD-API-KEY": token }; await request - .post( - url, - { ping: "ok" }, - { - headers: streamHeaders, - timeout: AUDIT_LOG_STREAM_TIMEOUT, - signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) - } - ) + .post(url, createPayload({ ping: "ok" }), { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + }) .catch((err) => { throw new BadRequestError({ message: `Failed to connect with Datadog: ${(err as Error)?.message}` }); }); @@ -42,15 +53,11 @@ export const DatadogProviderFactory = () => { const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json", "DD-API-KEY": token }; - await request.post( - url, - { ...auditLog, ddsource: "infisical", service: "audit-logs" }, - { - headers: streamHeaders, - timeout: AUDIT_LOG_STREAM_TIMEOUT, - signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) - } - ); + await request.post(url, createPayload(auditLog), { + headers: streamHeaders, + timeout: AUDIT_LOG_STREAM_TIMEOUT, + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + }); }; return { diff --git a/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-schemas.ts b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-schemas.ts index 7adae1ae6..0445d79f2 100644 --- a/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-schemas.ts +++ b/backend/src/ee/services/audit-log-stream/datadog/datadog-provider-schemas.ts @@ -2,6 +2,7 @@ import RE2 from "re2"; import { z } from "zod"; import { LogProvider } from "../audit-log-stream-enums"; +import { BaseProviderSchema } from "../audit-log-stream-schemas"; export const DatadogProviderCredentialsSchema = z.object({ url: z.string().url().trim().min(1).max(255), @@ -11,13 +12,13 @@ export const DatadogProviderCredentialsSchema = z.object({ .refine((val) => new RE2(/^[a-fA-F0-9]{32}$/).test(val), "Invalid Datadog API key format") }); -export const DatadogProviderSchema = z.object({ - provider: z.literal(LogProvider.Datadog), +const BaseDatadogProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Datadog) }); + +export const DatadogProviderSchema = BaseDatadogProviderSchema.extend({ credentials: DatadogProviderCredentialsSchema }); -export const SanitizedDatadogProviderSchema = z.object({ - provider: z.literal(LogProvider.Datadog), +export const SanitizedDatadogProviderSchema = BaseDatadogProviderSchema.extend({ credentials: DatadogProviderCredentialsSchema.pick({ url: true }) @@ -27,3 +28,11 @@ export const DatadogProviderListItemSchema = z.object({ name: z.literal("Datadog"), provider: z.literal(LogProvider.Datadog) }); + +export const CreateDatadogProviderLogStreamSchema = z.object({ + credentials: DatadogProviderCredentialsSchema +}); + +export const UpdateDatadogProviderLogStreamSchema = z.object({ + credentials: DatadogProviderCredentialsSchema +}); diff --git a/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-schemas.ts b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-schemas.ts index b03524951..ab28db616 100644 --- a/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-schemas.ts +++ b/backend/src/ee/services/audit-log-stream/splunk/splunk-provider-schemas.ts @@ -1,19 +1,45 @@ import { z } from "zod"; import { LogProvider } from "../audit-log-stream-enums"; +import { BaseProviderSchema } from "../audit-log-stream-schemas"; export const SplunkProviderCredentialsSchema = z.object({ - hostname: z.string().url().trim().min(1).max(255), + hostname: z + .string() + .trim() + .min(1) + .max(255) + .superRefine((val, ctx) => { + if (val.includes("://")) { + ctx.addIssue({ + code: "custom", + message: "Hostname should not include protocol" + }); + return; + } + + try { + const url = new URL(`https://${val}`); + if (url.hostname !== val) { + ctx.addIssue({ + code: "custom", + message: "Must be a valid hostname without port or path" + }); + } + } catch { + ctx.addIssue({ code: "custom", message: "Invalid hostname" }); + } + }), token: z.string().uuid().trim().min(1) }); -export const SplunkProviderSchema = z.object({ - provider: z.literal(LogProvider.Splunk), +const BaseSplunkProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Splunk) }); + +export const SplunkProviderSchema = BaseSplunkProviderSchema.extend({ credentials: SplunkProviderCredentialsSchema }); -export const SanitizedSplunkProviderSchema = z.object({ - provider: z.literal(LogProvider.Splunk), +export const SanitizedSplunkProviderSchema = BaseSplunkProviderSchema.extend({ credentials: SplunkProviderCredentialsSchema.pick({ hostname: true }) @@ -23,3 +49,11 @@ export const SplunkProviderListItemSchema = z.object({ name: z.literal("Splunk"), provider: z.literal(LogProvider.Splunk) }); + +export const CreateSplunkProviderLogStreamSchema = z.object({ + credentials: SplunkProviderCredentialsSchema +}); + +export const UpdateSplunkProviderLogStreamSchema = z.object({ + credentials: SplunkProviderCredentialsSchema +}); diff --git a/frontend/public/images/integrations/Datadog.png b/frontend/public/images/integrations/Datadog.png new file mode 100644 index 0000000000000000000000000000000000000000..7c1b33de053124638fc123ad8512b45694550053 GIT binary patch literal 89071 zcmZs?1ymi)vNk+u0t85a0KskJ8r&i1MuWS%yK9i(?(PyKI0SbH?(P!Y-T8OUdC$4` ze(PUrn6+0|SM^iUT~#wf$X6LLWJDZ9005A|;==L(@J1T)H~tm|GLllmjs^hGsb8fO zMZ|2S^r#B;sSEX}A@_oRE}8;;nnL}*_kUdwD%HP8|BOIL|Goa1V?bL7x&F?lE`Ut_ z*9DRJAAyh`e|i6x%zs4v&!7PfM8dzO{gv?lfcgJr|Bp0U2)uvI{~wwEBUb-EGXJI8 ze^~y5#DJ#Y@1p<4{QrXd@8o|_{XZ)FquoDx{d+RxUTB0{{(tcQBep;vVxRvS`5&nN z<@&dR|M&i1>;0d}{{chx<9|8p-`@GVIz*%Y*!kbW3-l=ej{n=0|I1MS8u|AU|EB&| z{r>{~JN{Q7#5Mn2>#ux>x&C4N*INH){=Z27#_)H%@UPbY;Qtr$f5}uKD$=Da{+IB7 zQ~xfc`;VM|R$SA%{|O1!RY!Rl1ppqMfR0O4%Qe0x2o;e@&BnL5d2H+E<>j>z7YYsm z84ZU4Z0MO-(KR}^efseFIzSGKN6E}DXC9EzFtKxd@%;Msi>ifp;qb=Q>n<-gjgEU+ z$MOA(7z~Q6$+y(H>-$$FL9^(Bp;as@r;gd9+ZU*JpFn(4NmVZ&sTCq~elI?i*oU>v ztRH+Jlh7~kTYH+qgrVRzS|AR|dp_%4zPp6Z>fL`s#9vQb5p7etY6w>{}&&xlk^^TX1dbtw^0F){@SXfZe73yf^ zjgaD8Tje=i@moaYc{Pk)XzF*C_#~=hj6_6{S~9>#J-{F2SA`}g%w?*}cqMF`tLo3| zEX-owagvtDdEDHRwCpV#8u{OMm(!Pn z!m&9Ajjocb2r>u72*C_CPyp}}9CiBZ+#8)-CiRGr(deMIgGkyoxMc8^(d#?J8|`3j zo~x}Cy^R{m^0WlR+jz*9jC5oy?-wH(WVmF-u;NhIvJyC1D;g zQCxCC_rt8q8wyyjbX0R!7ae|(ZT-x?N3!ja3OafR0E@rRy>Zz^IBm4~A8Ll_?9>)C zlun33SU|+x%@g^X5}CR`d@yC-1AwaAm{qYf?!r6^c};$a<-}Zx zq7nb2aU5hVM3ED-WL1xGx4nl6vD7UmDmwDsY!xL=qR8(E08kbb<>uPEJ`?mE(<{xv za_C&q?1TJdKmiQUvD`FbvlSAzQ6#hLW~6Mx;XR#SGHG8cj1&e3_;#m!uc?bmXZOKs zre>5(nw%`eMxNZPjX=YM2LM9C%lhvQZ<1`RdH9XYub+lwmT3O>rInDqis zz_`Oox-mHi7K-FcwQFYo?OZ-}xuS6+0A#m&oLu9+ITQsopHx_9`Y_)mDk_=HNLWJx z@Igqwd8#@N!9|93$8F?ryr6bjDV%tcym%x78t~;iZY*DA|GwPp^AO~>SydqR(afLV z-G^34B(DYi!L7*3aQw|rRCFPC2_qe*AI(Pd61m3<0LgJ*9Ar`MnI5jvou+nfmmA*e zu}Em_rVv9^_{wKo)7>X1ha@_>%IiMdh0HEJ`IC|c7648q50$;|+^v%?5tA6Ac+F8J z4Qb>PZp5rGPTv8(R_J_ub9L+nR`xQ5LG%S6jxS6S+|H|lkUgaAIULevVm~#NJJ%F` zGiVvf?o^8QDoRI*1prS!g&S_VbJ8YHt+aI*owU8bw(J^&y!H-hKp0CsmlJ0lMFZ!0 zk22-eN*-U5GCh3+ z8(nsmMfG930(VY#xQ->R!-P&053YWbQ`Z{;d^4Mmdm4A+p6A#b320JI??Z%jY(p^W z;emDZlv;|~T;`|g{T3#JL*7A0D|mn5H7aZdOL#!waBiL8#`Av1t4c+pnny!B8N;7? zVmEXM4(J$6L9g5Tsl#)3^rwwy?#B#47)Hp8QmYtbLrj&<=JVz>nEV|5#bR<&`|xY4 zNjK0x7bdS8V$d=v{l}0H?ksLBOjb8uu9FmeOZo7X75nvdV@L{7l^lymw>5~p+{v|Z z;7h|QdQ|U{-|K>yC6_atAKm@2ud^xHa4zs$LBCBQwY&SzzIiTNTH;Tuc*uglnw3e5 zjV#{9Fh3DRO3R?j1ecf#>yIBubDBAsH>`puAtrz;WsaXl3R9|zS z6doF~o$wu|u$Bp(Uxqf#lD3xKp(#tzy+}Z|OxR6M-tDoG+^A5Nr<$5hlj_zdJw;HN zPGJ@RTs7J)u_I@)khnYBkG@Ri@jn+7?H(wZk!CBayafc-&cIhzRuu~=&gM7w2?n1p zYvc<`Bs2nc!g3*zg|_lU&O(dS+~DTbS=CX}n~I?jOl|e)?~a27#+#~kG`sx5<(hQ} zEoPsmYxw8p>5c&wq@EyXz#S4-i>)>E&tKPBTifzP4hk~9nS6I81b|D9b_C?cIv2=c zT4enrl`w14xP|%{J)EY88w$9~T$te8mJtn??3{L&KG2cE!pM_bWW^5aS%L!GPwLSr zR4PIcu#fBbZM{QDzP-jC5kYd#{^jqBW8_AEAH0QmMNN5Des8%n{q=ODmkK|~A$_N! zbHCCypP<~ZIk2}GTb77bh@^pRIo@mYwLbt6=y=_0M6J+SW?fh_x>_EL&1TmrM!PeZ zGJ#mf;&Jl(vctwi(PayP(sQ~Ki)thF8qU{K`XekL`E_xfpw8uD+$&4lZF<7~Rv=)~ zD!#;%z2jUR3UHoxSV?S>b+6w9AFV|ydF-L={RskI(ut1X0FHM$?)nP5m#j&nV_xdl z4vF+iOtv4qYLsZUp}D6cnMlG&ORzvlTa0zk7>b>D z7(s%$R9SHDSokDj8Z@w&A3?!3lCFq#>3>*@%%8NcP7>bt9^tCMS6mMlSl8Rj zt~@jGu^E0``Vtl2%*%UQ67P<1)#_&j4V1O)a_xw&(9oLOXFSwW^UY^VWVe58Kmsbi zES?8tu#HH$?m%+>!}qM3wt+}SaV3;@*@jV-p$WP*GA+=gS7EzheX#n=|Z6>}1;;+;lo)o*;PdR}IG{X`pM%Kf=HPXk)`1 zZsTs-M=KRku;wQOu4R;crFf22U%onry}C79g!hKz%0hLh{P~!HKq?b~NU5qMkLgML zWx<@W1g(=}_D(D8UN zQO_rpy544CFV!))Iov_C@~w0d1?t=d7HDA(az4dpH&+qMdYLWdDwe9DRNX&GbVfQG zeh-}8)N?kvtPGs_pm7^pM(+kzRIGf)c8h>MK!jSS8+Sm>l6I}{Dp((UiRF+^lY_n^ zoF;(++6B2izx4KWmd3Or!E;N)0H+_OPiJP(~DW@wd}B8&e9XOiU{kx)6Ga8&_q?&dXp)s zR~~_Dz3xiNDn@n7P1OAFeYff$X^PQiX`a#aEZ^h>N}(_*(JZn)86)oqACPZ`1=0$6 zTgc3~+{}|MU?T97^(uBwGPgJWeN1yx1D+a}I2O%Y&l93M7 zdpckqg|ML;*Ez98+$BhJX(ScdPlSOI=w#Eg0>Zf+^cNa`dZgV(WTxsA zy2Y&&Y9_dI>H=lvShLU_yT>0XQ0Wj{5#_NNu52V&`z1$<`T6%FZOe*zR zZ0`wmncwMz^1%{KK8nuyDOhNUMx|gj?4`o$#!c(%Of&;67x+orzq}Tr|Ilb^%VuEi z<$#abBz{N$Y!*#6?$9`&I2k~GWFj^8np!K|wLlRE?qnA}ZMp#B;ZpXRO{A8> z8*THOff}3DJPpp0z};Fn4Xm%uR()+3ujL%EPtsh1QmT*Z%R)Z(H5%X@ks}A(+3@j& z{9&!zaC&*X(4;x6S^Oyh+!+j%L*K#i-a16YLLo1ZCFc zoXcq&d3563X@u4B07^TI#5ar-f%M!dw!72&oIYu6uMsAa8UeWyl5jvj#akq3_n+O9 zf^wvynJ+o(gODzQG+znu<)=f3K|b?RZJRdxItS@+D`-m72zyLuM5&a_=m3EvdU(Y3 znh}qw0b;Wn<1CkvtMTirpy7`fTuVDejS7aTtvvE<)-WKQE}W9b5gD{lL#jOx=4$&p?vVatSa`89_&VnS2!OfefAqe1)+WE z7Me7!K8`yKSvbFNSd>ea&0Rf}HM=9MHxyR~l}OOQZAna|`9ivr2pPgtJh{7-y&qAj z{?+nQq6q8k0h;0i?&}{^oEkaX+u9mgib*B0JS7v9-+b3E-$NGI?)#LXwGn3k-Nxgr zw!2`@?q@*Xj`UXh>1uHAG}mQhxYg^~wmB^=ZS8M4P8iC0s`NaLk??zT& z<7Iw8fB)Dp*b#{t$i7DiRWG_6F&3WD%Cm7KT7LJ@`(z_i#X7qVGy%D%e5t5uX#2``8M?Hcy) ze#YtL@KTeeVR(#f&;@oq560+c+SF~ZOYREO=2AAy3L2)YgBZ}hp#@&8Tc zd7e>ac~+sGs6aeM6cg#nZE|lju8*^+(Gwj4Qc?!-3xmL+%Mvf|vfCcyRG?FXpplYj z&~76HR5VJ&qIkGR#abu`+h^9aBcm%#`#wV71xBeI9%3%KosEZ5NdFWo@yDcELF))& zqV^eh)w@S#Ki4C%kliI5msI_EA_~R5JC*8q_YfIY%1l3!!p+A2Qwqy+TLLrN#W2ac zv1K!74-Tay;V}ePuN2kl<)@jH(*gtU91+x2YB0xKlE0^j4U@|7 z&vqicABaz|N1B%tDlON+MX2O~qU7e}Q z;vO++?(uMoi)GI8XZ!X7jLCt%QK|S%OBFb(ws7mY~JO{KjqWSxHRc zG?fISRU5hzAWjmi4|{R!d#x7ivWFE$RP`K-pshfVwUYrqLuZm(-tN&!!k0R!pVH*i|*?!ov_ZEbX2b{^IgBunUOyJJ$Z;~-7W*w-?;rXxiRDqoHV3R^-0dzRg^ zaB-avu8KugRAf&%$w;`)3I|F{8$THsce*P{!zuO;vuCCMec{k;wNg8A;7UNx4C%(sXkweYvGp=XI2=Y?au>`50YB&>>4Ov~f(?nnaSyC$J z9E&a@b(T!oOqr4hX(kWfxSie8wHQU%D?LkDd3oSEwn5(>@-)tj2itwDW|>{1SVw%` z5OPl9*5mT3mdi=%4GTpv5d%>l(3hN=-+q=ed-f@VZg({(CkYzFSRf9!`G*uG@0edDrxWeEb)30Nig3>9>xMgB?aa?zBwPQEHwpgzKGXS z!?Eq(pBekGZLNsw5}nnE5sR`&7F4mk8!)9l`7g!M(UQ4IOgx0G4e!6$t_CgTPU%mw zVnRafMsyg8rf%_}Q(&P`Dt7G5#s^+Ocw0;VdU~0w-M0Iukp_FjCye*1iC){0y(BsG zeb|qWW|Vc>eIQ@~^)}Ws(KkYms_}7m+v{l|ZbnC}LB@&bCF_I=XTqF{GOV_P1E_Wi zP$5q+t}PiGJg_B8^iV>|f8g-hKP8r@J;C!m+Iv)q`o%#IjSplQqPd_}=S1&#s2Oh?1Px24$@EbrcJx{J)}9@`vocBkmme3A)wNqrNW;k)Meb^3ARIDL} zGp8mk#^G)oLiV3+I8;$~_~h)!Qc)~Au?>FeE_)|!JAqJNn{Jc0Ki(dpBtPW$heP7z;(7vkmF9cX6qBgEiz)6 z-(!bpXLBMobz;|EM$k0F3S@~U6unGf4Z;V+*lpi5j($%m&T|b**;e80hECF#QvtIq zx^Y1yd*)`vAsG>{roG>8nh>K4$wl7~l{VLn*Y9F$i7P>(_%;HQLQg|!{LB+$udB3e+ zfa_FD;lj`;yi-8=3vsA~eB$UCvf;?1T9&r$6?{Z_dkLd0(mj$SoW<2b9BYKHCWpuyO>4(jl$<7 zXwsGxkY(S6r@2IP2WWhaqIcqG6u;<$WmKt@BArO)_G9WZjAjd6-V}g7Xnt)?&?Gw= zw_{Yfv}>wH@NaYEgVGTz2)*SRKKaPc56lAg_)ETeq_v=Em_=hQSU)B}}od zeQiiMTLiA{st!b4nN0l7rrMS`L`nKKu%8DFE0OOa}_q6rmAn6=BhGm z$>G*A_*jFq}(>8=4ITg<{vV?W@A#$QV=N$Hd7e_B^_fnD&h&)CH7R^JBlw`R^dBGVeUh zfqoF4guH#*LJHO$)>1Az2i+dt(co}4hH6^r5g2ym|7jKHNHgDHO*Wh~^?qt^$qZ_s zKV*CaH!son-zv`pjQi-nYGP^LD`g!}C=~JHq1!!OgnktLg=f$ts0D^Xq8^{eq-#ue zo@QfA5jKgniX z*S0;e2?>ai6HiOL>ys1Sa1A)|quda?)xO7|l+1qhtN-1K*hzdh2s^EvH3j2?y1p>! z{+r*|`V{aIx}Wc!=@E*!(1zpoy`|G??QYuW)y=<^ToUT6z0|<=Z5ItJNDiK<ZEzttM|xcGP*(XPD5Ch2cPi85wJd8x@5lT?A1>l;N&Y8XT+pD9KxrG(1)a_mM6QKOJQ1K7XrkAbCg#Ik zIVN#YeY1e0g`&oYIlqvF!QzQ)iJc~Fy-@>#EAK_TiM8G7h+~v~s5%SWttD%pq_BvT zG-d4zH%c0fiItJ|L;mJ9z}toSidx>CZz2Tds6jr%vot;#%5k@kp%1%YF{SHc;Y`Ud zrV^~|oS%?7iAlL6RSz=KBHJhn4KUHodd0TgKgX~qKJMMWls5g?M>}^LQ+?slkqD)? zO%4W;*COKbgatToBW3!6`w6cJKZw%TJ8nATZ+DSNOF=7A)TLtV5Ta<*Uz5A2B^U`? zRQ_%zOCLm|jPlC7Tbgc#cq>|t(3}X#Sp2DJYKHggCj((Ts*z=o2_) zt|$m5r#O`1D=p~n+nBSKxAmht{gm|!>kwclZbpvqb>N+t;CRUYIK}IBKSge{RpA?q zBbL>ZB2Zk#!h)*O+E4erm$H_N_QD(y$~#$N{OkD;*ruA$1HWP!2)tpYIt@o*OWv&8 zX4BhP(50imCpmXa%*HeR^mP19il$LX=@zjvaVT@yz?p&fgOeJMj$es5)XMNSlr#P} z9ZjNg$9>dLgq$w(Ixt7ai+27BT80PqAi5%zGcRBE=@&twead zA6Q3GzJSFK)$?aML~NYcuM(}q9NHJ@N^!@K`d~ro;G&cmi{^;}i@m`@4rT&1TdVD} zUQQoZj(z68jab+6&2VukI^oZfWm)~%+zznvHWo?dx9~^gkM%5uM%t9L;&l+F73QJ zBP#?J+~K6yz;I?Z?$~0x8a;z1J$ksVP|IO}CVpq8!A|2AOaWCX(u5&#qR6TaGy6qt z7tGO34%W4HV!mh=p%D^S<0BOAvKzMNadBpFXK<^9x=AK?8P30H1_kG`S@{Q4DI+g`qOh2EpgGgMPDN%+}&tT#F6pVW(L~u97zgUQ~ z5uV5Uh`Q3Q`3BXIxgnKDLJrCj_r0h!8_0xC6>0hCC&dQOFDMlQa%siYoEyDuJxptu z8cFuGQ(PbOy>kwh-sBsW5N+_PBRUGRcBCe=6XN|SbJrzyp8ZXX$vIt&?$n8{eZ$W& zGL9(lMgdv6lGuj+(ZvnXQWy?~@iPZhnORd;3xed27<`ba#KVpp>(yd9u5G-cr`7d~ zwlFJh&BnJ#8%+s$$7qWExt#$dbBe^1qS6RtS%6qd5}OBVuy9a)W5P6a;fW?QBk&86IX`0Gc}RkQ_5^jlD|`t{MSb8q zefuZ9p~aPF|NV077+c{D)FeKwqJlZ)9h?n2@77fr*-VoaCsNDmcQ~RabI#ESqxusb zU=G%ETjat<=Y2PLH@3@a0hYd#Buh$A8W&kd;+0#L`hK|r99pW0DnZ*|&`U!5{#%|1 zvzq72P^t({hPl+!z@rv~_JAXZP;*c{m+(vDr(1ikh6Re>65Ja}JwS5L8j5{hfS z!E0pNNpUV1mF|o&BnvjK=(A5)A;u9AD4a(bX(#>)fh_Zv!0%wQ+&QP_>HULf>w#-oIcaOenBxxLZI_cOPKcX~~BS|1_ z#+#i0lSgr$XF5}4@g z7fk(-naft?5S!TIo1uc9`0YL)(q1L{bJCWk_Bas1mHi3df5WbgK*~_$lL9E!crJ`( z9WlGK_`AH^YOTFDx0K<+p?R`DSK%|%Xy!e`aG7*JLIqXmG6d9Gkx?1Ry-V(fZ=g62 z=R%m(XVPiMe$lz`W=)C|=w*pBR{QbEiVgRifodZx!B9x9B;Y$O)W(dvc~?B^8HU;U zn8j~No?yYaLG4)iSkyHr2gPkIG}&f`PW2u|4(7HcDmnx`G5To>$Omxd%4>*ELsx%N z=negy7oYmjt5LV~e^3#t>~@o?QWr)IQdBT`;HB1+rNqZx{f6Gj-sB|9#E-y3fO>8wXN_|0s)R1TP z!3s>Z_#;La<#Q|1xgmxxr?kq4E3bkCo>W$<{F6p$V+R=$~UrHpAE;B)78- zOd3v^c3M%t2XW?I~uy7bn@(DFPxx#r?j}0dCHMRpPkuG!imL3vfNq-)kMd7yB z)x}yZHq}9DE`wA~N${f`bUesx(jM#nA$m@&3E7!eM`F;82Yr}{*FDCST=W~~&tHa{ z?#yoA*a&g>xdldlhx0?%xv>mNM5-RV5pc0T;W8gQ`-8ShO_N*`naXrAWKTJO!9nKf z{Lw8h+#fB~*%6b9!r6c=S;!qe&h??dpL1_#B3nyD{Q~i!mpwwKf!O3-7L-#}exKO@ zaRUq>w7T-ue6lv{{Ckz9TYnA;icUtB8UD|LBD64KrUOj4)kC|ezoeyco795la;tYv-sbEC zSknc3$*(?+5*<6im(ZqR`|Vc<{sw^(mwzx|;v9~DyLU$;FXkz~r_G8FkL z1d|M7tW9=q2%pV)bcw6OuxJ9RkiQ%cNbcW`e?3V>sQXP&8HM+!o!ng#ej$e!9Zo>v zd}&`65%0s&_JZE~7_{<|jZJ4JAOBG!EVAkP8`g^O{W37k9_|*KK1DEn z-gD55Icu7Qdxa%;?n;-M4a!0(R@>eNyTMoGt6%Y7kPl*kGQTMAuAz>t`B7XEwQgQnA10upxteor+1ra{7viw zlKgG0{8R#2Rkza*2THQequ%2_jwjK?LWuUeUd`L`Mdev7d+Y}Tz4;2Qp5iPnHF_#8 zyi`B9kVf=rnhwDnJ7%WP%pqtkW;>~D9=9i2 zbL8!ks?w%r!3n~3XM9H!1@*)rGk$R~hh$Joe9sira3METT3-V(0~<*dW*w@*0JxJE zk+{A?MrpfMbq;o3X(AJ2#u!=E!Ex#t%v0kHXR)2vUV~CDL0*x_l!rNApOs$MJqE-S zFldWuhnJ)JY9+D9R%gQcvI?bs5QTgY_;%J8WMtW&z_OT}TR9rUG-vfhz7)O*bQ2S2-FWH&we`n{)Z@y~0Bu+xCj863-Dc z?>mrDgsiWZrH2M2BKLJqG2pj(#u{R=ztlrlzH1U0ZBAe&Bti6C3~a%p(F!*ft_2wx zX~ybeg@u!z@2U7dmonOHm8hciuV4dI=*4i>5i(fzsq9%T1cy<^cqu}NhfQystJeDC zeEBB6J$FaTUkEO}M;mw$M9Y4piBs1doN|0C8j8p-wi-n@nf*ynFTd=yDmr zXfB0!j3cdUqUFqSs#oXaKtG{^q@q^Dv>;`m&XvqWfCVJRENlR5tTlu0ZqGA$*s{@GCf ziqIb>Qe+$1^X~372I~x!`itys=kL;bZ>KBLH zQn1;spOh)uqb*^GSaT0_e`T@7Ic{M<^zGZxUYa+hKMF$W3%Wf%TECv1eY;j7CPSNT zd_U6Y#@3VfCPsBuB!^J?QFS4@5o=^yX*f&V^^f`6I>m|*vojff(bww%N*%h8@6x}% z?TEW|i<`YEZ_E`4q8vh;+7vuS<-5M$B+x@P$Je? zS-K_k^>@)HXw9^-sjL@GOrg{GgJ2c4pqf>;lj*?57ZS4*m`~lwT;b|D_Kwgky^E&p zrMT21Wc0J-LjlGjr)0e=4?ppS*;YQIMW1r8gs36*N31SrNUoY6*tNu7Fx82^BoYp8 z+&juKJLuGFn1gbdUNzBEjTTfTLOr7W$SZ5evYkA}G1&O^ug!ce^b?lIo`i7FrOH2` zv7f29{^}@mn<{Be20IS##=?&bHA#m)I)oq$lke+Mh7?i19j?a}92!jD0s4ToI^$Sfx3b~AdKF=*nmN{;7liYtHaVo55iyvqB`%wF9oo;eMOS*HH zQA=W+RC!zr(Y1{$nWUQGK<(6h&$PY}ajJX|kW4xdvr&5UC7TYzMD*xI*c-$=*m>BB+9kT-dA z%6K<9vlpM7co8!}pj-1b?Vf2PBLY0#rnX3oqGMOW;Y0%~us{u~u9Ihc^}T!^$jOCjY@1L zXMCT_M{+lBjh^CU!gTc;nt*xCLIo9m$=R?Cpy$hqpJRX<{@xWxmi_Zh=PGqu8)aO8 zeC;P2+EXF>y%`22n1UHh*~KR2tYitgnvM@kTjFk#bIBbzk9JT9reaX@7Btj;O~|C3 zreYdri+>`GV|!Jw&{PD#fm?0(?Lx9rb$$IE#CZl_x0Gfe7vy<<>($2_ zx8%eC=4+Rhs{W+y57mqmBhUrI{I2?X-xHHVkH`Fk>MKb8B+xg)%>E8-bVrljLqI4o zneb|SNPCASH^GLaO_M2Lk8sFVfsGg)eKD8#(_Q~yM*|2d!^f8x)Nn#A`ylN~gzkXrjKBs-MUwux51R91H5@pn zfTE)bC;`j4Wdt376&FR4v|_Nw;cZf`XqKsL#|~mX3HQt^!{Cv4$q5ZwB!RxM>VCo& zg-*qRVGYCiTwb04tK5ojgS}NMF7rsfqs7)GoeOUbu0}Jccyh#x4AB-DMB-G0WHPGU zP}pg9oDvY6j5_4ILdn_ZZSmLy*~tZlp}`Gz8hvA_Yf#^dk{4R!{a%>7ytX{1s*sOi ztZx%RT(-<)#hUcI1!zK=Wg}woOf@#i`*}9l^4ENfaC`H;vi?yyn;I9#&bGIFrjz<7 zRhXrvSYK;ie`;``fBju7A`gFvkF>R<{Z!;0-=0kvY!Z{O<#LKt_{@icB7Xxu{tel| zQcw6<0Yf~5;@W&nYQ+|l1=|{l3s|&O%H{{c*o?8kC?x&aF3{jqKfQ&9MEFeJ>ozHe z=^3qjrn4lQsuzl`FepwlaiQsm(P!T$oFZxd2o#I!DZ9?SmroeCk-k$Iq_qEJHPRL2)*IcuOY!V& zo(Jl)ywsuVl>3Mz?zSj$*?BPeCpuX41*J~)aUuzq;o&QDEg9x<0I92mJA7kw+i}WT zy~W@J^`9)UsLTvmX@jH3kGAonr+8GS%22Ajf_!Az2{B*8s@?JyM+mOB6K;?&y>zE; zHQ0JWn@|wMHALtOc;~E7vVVWBRXwgyGhz51!yE1u_co|k3WQo&j9rHE!w?eHgG|M} zi@0WDHu+C45?v9S_w^rX$vO=qJ2g@nuD10b*ktQbq*}r;Q}|6czw$2h&qpe9l-1`4 z$E&b>h4zqruq8w&o)f5aL3|dCHm%K4JyV{2YwuscT>(EbEsmC+8@AYTM5AA(M!crJ zURfP!EWeuQSjmN=O$KGu<#iKKqgqPE| z>O{^q`DD>bezhFcNjKug{B5Bl&3yah!)}}}wZeHKlM`iQ$U^&A;@Sn;`g^U#7VE>> z?e~2Tk^WmOIsvhpwHhb>tyqw>mA?Xg6FL#k%9iM-v6sB1! zH57zf@>?$4$T*=}PyUKMzh$tBtI}nMkm$ooo$qrVqf355k0TM(>F}P2^+)xq?YP*u zeT#|wAqod$rI;=3uw%V%Tjp~|pk6f{?2gr{7NQTlV4wq9Rx!5t208I;_BDdUWhzLi zeHh%T@>XM#&`JG|Y&fS7_pTTR@quq5MiR^0qm0EGvfQvGAaR~QE8q`*pI^hE!xI?l z0e_8_n}_WzxG&(=k$pGGlCy%MFWu3OJpC)TDTwMMY4o;pHC8z&*0&7@b|x;7AsA$* zrsEypLf#+qrRfN}5kz5Oe6HS-^WzuajEdxHMBvrUTx|17DwGL^^-SDm?9XqlaZ_Qi z7oK$xTV1PAg4Ke(vY*Beep@zoBqFkHjE}Y+ z?t_a{uI31oqsbIa|$_(w9VP%btG15YToi*; z$GNG(EEI4hpk7NJ5$dxU=9Z+-3D|LSh(pCnvvlTN8cW!h*sSEWmw{Em25F zP7kUx^{B=?9mq;ImU{GzmSex)_(Ev^=9$7}J^drr7jZLwk#DW_`ad&a+Zv9^MaOa_yGN--aFdwdqQ4U+x8-98)Pd^I`YX*sZd8Br>?%DMc+&!7LSBh z;dhBUIu9b>ER*zY&lxR(fdzx2k7fhCU)UsUCr!}3#Pw7X{af`vB@2?Is=_-_wO5ga zC>DPH=mbkof~8Aa+s%gVdsT%V^cZbnCGKAXEgIWZmO-ogS}E*E{NbABdx@vxnbgN0 zkoWuetIU7uiLrEtAIOwo1a5DuO2NkV1?y;}>4xVTVu-kIs7DF>iLt>Ch?KENgHJyX2kilKKzv`^;r}jK=^^~*f8YAu{c^G z0{Df}1KVif_nE)GspMZx!=!9;*)ea>Qmy04&)?}+`Uv;M1pA@q;2?ODUgt}^cympE zXwA~}uhLc_1-0bfyXgi|ZBy&5sjbtR>4K;%RH5ei_50$2XA8c7Se6ovV%43ZzGww` z{$|B4O}X@ks{r@!I*B{0A+^EFG`DaBU=(OMqXUqJisYagzJ#dYF=IB)oGx5gD^4Xg zjPYh|!Oi9DJJsjCb58pZ8vl)!-zcF$hJ#R;P6@&oI4G>qxfJxEkB8UdL0mhsq)B>~ zuh*7OhsIqxD>t!7j%t3GuCoakM%q?T$wsUpzn2L&Y3A}HFNj%bkYDkfroHFVG{0AE zoClIW4WVtZ^Z9SP5WOFrqTLq>bc*$FIJ0%5p(T}N#pH6z)2 z{2nt*kG;+_gjypGUOYvDQvMt$d}T(`V+>yT^rd8u{LOXxvMhbiyiY%a7=B?7L&*qP z>9bj#ZKieKS9$rR0o}>$4YFK552D&lR~Z0r zpU7$j^jxv2D7TRC=R3p5auswW-_H-19|k|?*{~dZHu@MN$;8G|gze;2`}Bt@RxU_q z0*xxHHa{C#tP_5W4X<3rXBT^jL5vqEC)iep@j&t9Ju^4 z=lpC9=D{5I7#FbcPSE?=r^CncD|io6K`r?cPJ6n(*rXL9-oj8Dv4=R#2i$SACZ-~B zL6xCwceyv>;wD4H#mPl&J*x+2+y^OcNt)qR(Qn?+6`!v5eNc}Tn`?pmtMSGGqx|T; zFoL;>yWp1xZ@wN3M&j^QJrOAKX(P@LqCfk8UDRS;uM8iaN>-1%e9jg@t4{_qy%?p#VRl zfiFQ4isb8i0;{B;HT>`_JzcZ;^=%`bK^q11gfGS~V@2#b?IVQRaJ#Q6^1~`~+3t|1 zoxzI)0j{B_DrlvrQ{vMumBE@(Ra8OpWGuB_Q$@tn{~rLbKu^DFaVOC-rZrPYDK#89 zMbBUo(YZkqM6-Qv8)|_0&Om*|TgVL<3tN_^Lqmpur%7gNrLapzv`o|me^SbW|AX31 zsw5GabITzX27{G&6irCw387nq#4kglo1?J3w67EmxMGb z5W>;}mOA=@o)qy-G1TqmTs?{C z)^-R&llnwUtV!#Zqk)87g)#Hvyz+9`lHeB?LM?PA5G`fOJ55UP4=;zRS*C}Qi1LFV z0%J<}DYA=o$X2qomoe+71sd4&(GOc!Qp@D~8PQ@rcaRdMrbE>%12afOH76i~qy4MM zKGtd!mct7Lz?_;SM zY^k4w2J*flH)3q%^g;@~toV+W9 zn#Il<(LmuMEx8e6y)Ol#359u6lc@!Y29~46VqW@?`z=a=s+~MfLfUW{UQT*6+e*!2 z3&x?LudB$N7|Y*UfhJgDHeI0>n4N}}OPp#Zw;LXygPM(tBO%4jgm;rlMrz3cZ1`9U z8i<@lZpB#gv@{EvaP!feF66Y*#Xq9ujy)l_3+-uvn#GJKAzjIWS2N6OOAcZB0Xj5L zK3Y$1#aQTp05rvv&|}1Z$yqB)EC9TyJKWs=2-Ga_3<+sv0lZmMk4kb7dr^!Aq=+lz zUW}O+WJzd(q{%vSj+`|+T0)Cif_9L*#od6K{d`r_Nl$pO8{xglQLJ4g8p?h_ZpN6o zZ=MBBiitn3CFl6`FGkB19&ST!Rx}7|W(g-Dm7b8`t)y`i|0IX8HqRt9lsk;vjj?u{ z4QSGxgB>!+IlXc|N6XE>5Jv9hzuE{DGv$z&w(8)eq{$O}$Z;&z#!on9Hn|;Rw}Z0L zw8&0F$vNi-p%r3Y#*%v-h=Ph4$AprQUX;N*m0#=TL(X6wR-hs2a4fkUV>@!n&@{t$ z&qK%=`8_0fQ%+{&Zm8LJQ%FctW8jsH2X~su7VWz8Q^*!}B>)XcKlCB@W32b3-_f+2 z_pbIPC!8LP)-VP^1GI(L;9*!XLAz`Qg|rY#I{<{ zkgntwDS)x$m*aG3+UF6AGRQ$SIcOzQK_7Cb0|`*E!R<*%!H?j5EQ9t_12sE#cHW$J z-w14rBiq=63N#Yaft0|Q&w;@vG|dw8u!tPfdpBB36I4lVbUxY$^@?0TLW(W0z}u`X zxk&X`S3crR0E@ z#b~|xi@T89_^e(5_0nx2A!(mphIjej^i%X+?7lg{oMl_%3Ng0go zFEGHKil@HRyu%jAAfbOLxy^xWsF!s17ZTEoD0r1$4-cbvf)_kpeqV<=w#N}`Qu`P* zqI(of3SsO@?j6`uHm@BuC%*)8D3APDPwuim0qP~K4kaOVNQXBmj2}jCX#K0temCM>mkBi5we*U;GSKao-xJ3r-&) z*_(tkG!ot;;X)gFC3NJ~A!Qm98q!>fCDt_U88jvZJR!v}cIvPJ_UP`P7)thhUkI5P zBc70Z{Bjj4HmQgSn#D{*mXas znwqBFYJpsIsi(<3`rU_meVIf;I`=iaMP%nHqN;YCBT_W5>GGivvWFFwz=mflNjZ$2 zyAY^@JwN8{)Vt@>)KqeX+@fzH)T`nO2`MZdUZSFZ2A!I^BoR$|w4Ur@r4J=ERydUu z#8{WB$6=Q-U``D+p|SvS`Z|f+q0b|zSN2E}Qt3u`hvQd!6-`S$E}==qTeM^!J2M}R zS^WG-L5$T*iWTx)_oF%PIY z>|B9_#taw!LrP*yziTq=i^>^Jwk(3Yq|3h*k?Wn!hH7cL>q$s`6CeajPBJx%jW(e% zOX^Tk6l3~s*I-}vz$?_4&qgC;W|)48TyD_~sMd;b9}-f#Z4g4hr_?<5Qy^>zJV44~ z%sM*)_E|oRvrD4yz#0KmR07cea8O|I2un*_BoMI@7u!g|2_=@Jf*BUtHT*kU-VCxtPlJ-r#t*w(>^>Ukam zS18*QOfFTk*#y-Z+?&J{4)0gHCZ8O_I>46lXH!UVj9n~2GXnbQsje->aEYiBndC~L zenzO)z4IidxfXc69~P2h*y)q7rJ@@tkFnd=HL&lGej!xH7bdvI->yAQOE+ntS~KHF zOp8+B?aDs!A;&P^8L*{%i4Q4{v3>J2urKjIDZLY#Yl4g1KRk?FC**<#sueSy#55`m zUat1LN^%hMc__h_gBQsIFgA3a0rvf|NKbF=8w-~yi@!xKWA1N&Y6YGlF}0fvFIO?1 z9L7F~f-P%bk|$tn=$gy0FY*1m)4qR5a2Z37Q{*bzIhUbYQFBR5l@SuWnr5^=IgC9C zfK5F!$rCVkYJdUu1#~mh8)wSkLI%I}_aZnHyf%|-h;$s4zj?T1%9t3$1!UU9c=liQwVtk#!lrJ zB-m4)x1V0|{Ti;dV&G|Vjdo>FuQ8z{BKJ2R{R~S(N)iQpUNkYvIz7C2SzCZy>za=9R>plaT}4CdUzDo}zbrA?&*2S9CfJ?l5lI|0CITjf8p) zo<(9xIu5T@v5TC<%pEPT$u!YQo`SJkIoYtQ@LRt^xW)0Sy-03X5};m@3rI}22E$uP z@qd!D*v?|uR^UsXgz;oA?E1S9?qLe-lu0r>{WH|7>;Q>rX)3(cwu|I6)^<5;`*k*X z4#sZf{0I;6zaINYUf*Oxy=s?{n5x#mOBp8W$!Toe0NAEE>_d!sj#J-e!$TN?s!3Kq zU51LC{e^_&znVvo)a8%~HvND0?t(AwYfl6CLmKxA%synWDn$4<5SI%>H6f{OZkc-j zRART#g?33TLflCzPzBluNjILQ4YjyT-Sxe!`(|ct;oQ$X=kR>r#ZRBu)(H@?NXzDO zS|U=^KMHf&zbuAjyuYDakVQUVv}7KcbkoGKOrmx&EF&v@GSxDvHa0{ z&_i>Nuva^HQxtw064Et=3nASW9T{U14^4q}M0U?FrZqa-9{3PFvp8h0_WJ#C_-SP; z9rMrtNDuV?kx7J`>tG#`-9-WIv2RQN4D>+#boOp{H)Oz1BgWD#!Ss4a58OV3$sCyr z`-ogvLyIJ4-MJ9m_vIVx-8wf{z)v@vpLuBa;ai!#*O>dzV+z9*Ku8?_Ys# zsvDeO?-ss!7Jho}EjlI4eiqUrdtyu?akL)x5s6OTN}D7;Sv4NrQ{Ba0E*PB$KlPd6 z>eN)s-;TdA2^JEGo*Yw2n;d+zIv3q=$YX(c^no&e#qciDY#Afp%$HeyS9| z)AMsEH5cHg=&B|J9wt2l~Y5#lh=h=VsNs_YfFg$f)Hk}bPR-ngZ`)DTBx(Ond${#e2)+zpE=~X+C zUi{wqcax-aUJFmP)X*8TPon3f?r|m+dweKFEVc0M6|_%e&f}AYB8|N(&s0+WRc9kS zRlkkSSaTbCPTGHEa%1{J#8&S&zD^5$I(G2Y$10Fq-Bn9!W0Ze4{Ve>{C!4OQ9FhYu zU}F}OI(}aiBDPwzI;55Uxb5vt<4bT+75xv)2`K-mupWM@dz-GPnFKN6-8D=qoSq92 zYn_;%pq-v9Yrf^=hP}B_{7=zgmwdc=PK5H`iYCBMIeX}cV97#=345<&Qo)!*5V6;! zHN~`4@WCHv{%O#oRqwXc|D1tuD)L(Sz5Hqfx&7>+H>;n;dMF;E% zXt@}Jf5D5Au4;Jd!aO=5+_Dg2M!&60F7pP6SS;tJu`!CfnqGmY_AjRsBG*C;**iaA zQo+C|L@ajp=30uoDi_03vQnb#!u*gH(Ma)-1rQW3s)CF#BwF~jiorLYdAbL zaVzZ?+%gnmPQNisVck^_vE9z84^dpSZxTE;YE3}vg)cx1YAIw2;Wh0Lv0eVOrecbF znyTTcoy!umUU(X~=+|q67EQcER$A5R3nk-JT)nc*898# zVp4Qkgeg3HCq!H@^z`c#w}j1k@YMJ+TCb=SVpK;fQwV;z2O=)mHMp4Kltktzc&c!3#0_7LDW39X<8mcoDhF>EX5^dBjBlu7ihhA>L5mC3}H&kKZJ-IzTJ~WaY$AN zJQZC`>up&NF>634Q~7Q(L>zJ3W#tri9KRpF8j+yw=5L3X_4GQX@;48ZK*SL*ZAnm^ z5jMbA8_Q_9py+*wSub7Bl(sE|h%0VrrZ^*Lgs)y+OWV!(5MtKQ#(=5Rd!QG9Q76JFdJwMsY*)ZuqL9l9ro&H4Kyb&zVwi3L*~K zxHh0T;Y<~Lb$^7GOMDH(q2 z2qF%tYlu)>&~!C?HMxeiyDJ}t$sM&!>8ni;amo01K!-K6w9>@hp#r)(suiXz%VJzW*WEr3=zj1Iv1nZ?f4}4>dGH! zyD&Q!W=Y1h1XDZG0uk4IF*cxBt*IKm+I5E3TYdtD$;}~CYuyMD*W^CdM6p^^M--l_ zpHJ(xPJv-Epo^(B4QYpnYg!5`DK;xV9EYdYFQN4k2Zq5cd3hF73*Q_D5$9B8QfwA! z$bqMZH3YQY6%$~Vj5^0O)*ge1b8_y?qF5~PLj^n)zd4}wYA3@i$@x<;Q)_z#BJO#1 za7eL{|6Q+FVzggF1&osDfhU<-tN|hp>bHPmugcMR@YUp2+OK&O%#wlQm|AT0`w($Y zeC0zFdu5iwS2xV0{bpCgEIFUWG-f>w5eGfnSV^(goMZ6S@HMnwxHJx<M>(^Gg;BW~{k^u>(5t$Z;h?CYYNl>iRc^SNQax7i&L@7*?sxqb# zT(KS^ZrVMIVy6e|ba49DmGuftk`r$+oiTGE;-->;trRO2j)JdlKSCFT59Yxjd1?#O z$?AZJqelG_qgW`obrF2kXB%CRn7I)KNuTRTz3TOsREqrVENc+X-_d z+Q78*s#n}qGGqnCLTCEJSMke2I-&VW7$k=~naQ+Fn>GDk?MI&>xBx4xN7}tl@#kFR>E7)x6%!> z2f!Sel*zOTw?o8PFRv}8SZ7ZhzS^HfH-y>iVT?=|&9vs-4iRUyU)n^mPQx(xYC{bj zQF9)~$oisy={!6FBJSF?jAEU}eE4eAIXWWo+D9-&N>+zVr~DtqGSe6=^5 zuBf>IrbzqaA=3$0?}UiEets=Mu}o15e3iG9t_YXr!VrnRu!8BtmVXElhdrGcQ0%gH z5qy;~gszCKn*>87I`Dg@weDVsI4tL}a*ADM4~Mrp5_CoQcrFZ)FIt(_zH1=jvgejl z?6U6>c%JGlkt*RP2Bb=58Gi2hAOs{YkL|pdN zJ2C!M{WE)a{20@jy#f40kO~29GJTi?RS-!o0i{SL3Ne6c1#KAsDzTxYpojoUC@4q= z0c`*s8Umm-rL=3?ZM*xweA{mOEle_ZCNp#AzW4e3IDV2O_vF6sInSYIln%6us!pwh zd91%D6dFg<;9uegLS$;MW`Ux~zH@n^svTWm9?s?oLLt#|EBq^MBBn;v`gIqg2-y}V zi>hY74)f5)E)oif*$<#Y?_?n|wTJ%!MG@DJ<)W&X888uVhekr7aPocV5Z_veOl>@O z$wU>`;S^EUbeKv0RiRL*DuNEPV#VBu>URc34=wesD63rsOvG8VT|$5gS3rj^cS=MM z#T0JX9(%U_gsY<_9zTA(>BMC_7mhetbvILLE9G_uD0-}GDazWM1v4p#lMtYs=^Fe? zA1L8Q^vUeH1HSvT`TMEs_kT9@%cO#`-xN&BN=xk2V#VlX$Bvdut=yR9MAV>ujYV0T zj=)Sjhi6OeQSu4sU^|^B;Y5_&J?@M6ZIy4M8rM6&`EJvmbtHIsYqCL6BmbT#t8p$& zC26449_6)z4$dzBl&~R6>0TV4T6r0M6eNDsXYCi72a0ssm=?nYmDE zjs8*WfDY&DN|+FJ>-BEpWE=F*vI~wBjjCHN!N?q40g4t$O+{HbGhHy1$L%tu)@UO1 z7&}G6g6QPL7k@%P9(~=Q?LQ?5BRhejMa4U!tjr=8%w_jiQfm|kJxbe27!b8u)*{P; zK-q>BU9Dr6fc35bMG04XQC41qGp38CDwo=#Z!(~R>(D0>YDD%|R%B%$7;k@hA+C)C zs_sDz6dk4rO=~AOCbue%x`ACjiCMX`gcnLerMPRiW2X&5@p3r zhRJw~T1yyk!Kn}PdI(WyH5%Oywc*JsxjJ3~a_R^uO1#-pl=VRY%%<`?p;{XEAL#Mf zCLs!`@oSPCs2$gTW=zeI02IFK07ZvRb3|D?Dq%VYjtbSzz5dX{^O_KaQep2T4YlUT zU9(gIu;>UVI_y3s%8FeH)2V2eCsZp_k3g5{IYJadBZ~WUS^o&Nw(r4fCq=hLAA_R9 zo%*7z*&Sgz+SbNGwerCo=+dH&5QUIEGv@B_Pdl!E8MT!0xF~?*-njjssF3xID66VD z%;)0i1fklfmkV9i#R^f#jp#Ka&5K&{07Zj+o7LYYVlLNu3Dw5VMCdZ~ zn1lgQ)uQ(%c~MJc2kMuLF76jfLD4`f7Il3OGrDj|s1}Cbfi91~ldvFa-7H^2EoIyp zy%B?39ys7JDyYuglKa*YA@qX(?apDs)GX*1uiTQWz~HNru5ls{}2jR{YvPwwTFZWQIm9p!cS{P zh=0ew1Vw>PM@3oPN?=Z3*Og$=q9o|k|Eds$N8Xxjgo0K!gt^|pC*`Uw$7lbH0mhRRN8l|t-6@M~s&xi5or&mN>W(|aPRlXrXq9)MEvsj42 zB4>IjLgm{xkBL9yhQfICuMb3B=dxi=+TpGe9NG+>935HsLix11G_xNs z8w9=9)f1x7tFzjMkaDiAFE!hF9)_bc=ki2Z^*Y0>wBYu*Q_gbeRn|+0Le4!m$%)YN zq~4PnwffNmqtX1^$)c=12Vq+24a+5PRCow_X#<5QYX|5->XXCUpC; zN{B+NZ5G1e%$PP(gYqIT3`U;ag`%v?J~o(F>8HsOC|a-$y1g_)h(fEpxeH;j>WdVq z!K>F{G+K8_l;u9R3g+dxwn+j-vFo8*{uLn#t*QMG9?rDyqy{4sVKnO3SkyJ~5lqZc zazz3}jemk}U+xs5kV;tLL72Ga4VUWuyaGm}{Q9DQ^;W`Ql+;wz zwI>m#<~*x3k7>g>-iMrZ!fw`rBkstvecOY%t zj*?^{3ZYSx5l*g>JyNZ9|AMi|)n3$9CmtrZ{-y+g^4`yYetnk+QRw8YupzAe)htG; zGyEEiMXtf3uDgq2a;~*s3F)`w0RnLF6(I_pMx8teuf%#%omL;YU@VGn#f2hK%(l9Y zkZz3+A^^k22~p_0vNinAgYi<0yoT8@6lw2w6Lsx!z~sD1b2EkX+B6IS$QmU?q0{yV z!pxBsC)H`24r9@fIijv_3Sf4g#6?1SO`C)ORJ!tVV&QjLwfVJzBxOw=`? zA5712c!!WqpL!7h=X4>&jZ%$b7Ys#N--x>Q9ESO2d|8|)q{q?s5s+U-2vNwqF&5!hI@&G1jq3+P zk!`c6YePH~plxj{q{rEn2uPP5LKHF&*CYI#54wqOu?Ju%(r$^uR(PQR*MTcSI&7MR zfULYBL?QD*XGFlHfz>8~IbbO2{fVe+jTb67b7;Jff5)8&$oc_76f!#wA_BA*7l>~I zC&5s(I6>4^Tm~f!EzT73C*fTLCcT%03{j)L9z;OeH{#o|VK5Y}Zq1hUsHf#F5`SpP z76it-Swe;=^A{H)z;pYq__U#~7e=Cz1){D2!!#(tIb@{xL09@CKrgr@REXTmvk?iM z>xxgwhiotsJvb`rTDldAu&v%MRI>?52+;H#2^peMZy^#&=eorg`>s_m5+!yQb+t}~ zD$3r;5ej})xKD#y3HfAqXC{~Dq_ny;vhiO&@7k?SLP~jZo?Xbd8N ziH5!+>WVqwfg-f^-?@cqkXhtGfLgScr%-021$*XvRX^_dpfCD-T+(Ufy3YUpOXu8m z=jR>n@NVzpSEqiyXM2iWo6=q9s5?Clxk7&WCW`6idbA;>np;odMr#J}r{Pj3u`fhhZ2o~UctWT-<+YL+9_pybmC*w9mAWeFpD zwOc)J(wX^Q4MEG8zv@WQv^j1Oq1X~cgtu6HI<^}IqWO!HMP0`Vppx}*b|K&PbVR_C zHd*&{x6P(^eteMTL`WocdS{1S=pXkzh=`9{i!TdGU?B4T7$fTHx*jUYm{niMx1L83 zu(I#Oiawd$c=L?ZvFQjMS4HaFHbPUzA|euZh);VxFb;VS=ZLy4^@UQLL(U5MG|_>8 zr5B6k*kdO&`{l^DmpuqA$MvF-VtEVtYKVkKy~L-{lVKckq_z@uZEpd^v^Xl{!`(v& zT*W(fv6ST5%NiWcw;?RDOX`Z%;Yy)G`%; z%bso*>&Uz`;oaSpr4EG4hs_Il;ZTW4a2!q$Up~JB1JS=5i@IVOLNx^^TFG;${!j$Y zw!$q|F@8X^Ed>{y2&eu%#OhAIj)+*dLwu=P>x6MAzrHB!mJ_NOJ1|q8LG}~Z5jam# zo>)R&?6miCOA&5AO%ThQR)I+9zf627k9WX0wEw0kEbdz~y_Hapt7MKmg+`Ym7!OX0;;O#+co;N9 z!sPa?Md)lUBB7+E__DJM#vxC8QP_?yP|%sx3xuAqq{xL}tc?|wxm(7)xPQI_5%K8u z5)q-~6_*iy+Pt&k%bY|QhqM`@u-Pp%D5&ComC)aY_d`&2cNc}lPP>rgL4-U$eNui$ z_bL$qV;_nymkz@~6hB_n71P`S1vw@Sx-0Y-_pccUhU0u=QI&o6@s3|2QZlX|8X@$D zIz=9YpJ$i&vY|PQL)!Z-MPcn-P?1(yoGkRG)+Go|?oLsYyW6WDCAknWXI6|5`qPw3 zM8LUE#D~m=Fc5WGDhg|t4kcx*A0_ms;gtwZzk8yj5xYtXT!^3+u|j`Y5}$#v(+;%~ zUk1WJwELJSY;zXWlrig}&>!r}OA!?9+9FZV-Civ|b0UJYr)IV&u2MtTU6?JtOv!?A z=*~S+*rxSR)x2@?JL=X+LvZq!iE_$stVnwqk>p7GPUs0gmpBo2sYk_^sktx?Wql(G zYn%&ZJ@`QAH>0u;r2cP+YBml!@6-@cXBOWTdeW4k2)nV}#h1DdU>ve-7KODr2z70_ zAwoaT@E}N)aiW%g#(ne_B5n5^c?zA3*AQ-H$HbQrB`^>bi^6hV%7C&Q9UBYv?1y0e z=T%Wk-P4sBh_qF6^W->k=H&O3VZqWjbd>Nw<2g|O+_7Z`lLA# zdER%+J6eU_vlinEW7B6Slj4y1TFnnQAOd^ZKW{6^D`7jPWI4TO8S_ZY9Y2HRG?06D_m)%a3;6D3a834YUMvtFYXb3BBix~0ct9~#J zCGHS~jm(1LN@sKv%er#df#5lti3(zFre1_myvv1rsNWf3<(*kBzRX^y!7w!R6;arp zl~CP(XS5WHayR`KYQfoC6fl1DdKjoAR-Q)*w`~Zkd4=N3-M$$x45d#Mg)Qg^W%2Da z_5o_cbwbRq<^4$+Fwmr{LOz^IL^x?HM~E*u?|EPts+b}Qt6B}^Wt28QDpvHv=ycSE z>ztTg-J*21F-D`zhG!5?v#y9QF?}w>FjRWnF6zpe4hvZI@gA|D?hjn34aXocyB;Mr z80^uYWO*JfO+`56)~&XP8-}va<%z;x@WKLYMH|I(+`Yd=t;`=JCU@+>J{Zk$ZJ9ie z+`qnsFzS3vd~p|N!!Tr9oGA+1?1B~8-Ww@ab80tg=S({>x2c_8hVeX|ZpiZ}wuKAf zGi<8(baNF9L!J&XqOe|Bu!ak-h_$4gMlDr*FQ(SBuM1HjzKuMOzUhzfIWkUsy7ym@ zVKNTnh{AfVgEf5keh;yf31d-9lZ(a7PIW<)D7{}O^qhnhHiS+71o7$YB#>b;<|T;2 z>b|7GBDT~M3)$1rg}^DaL*}ak9z=_*U(1D_bM`#KCT)uNG_oJaI9p>yVcp(`WpwE& zmQmg?9kun@t72X|T3kl-=ojafXHn)0lMyayO~j{#sUYLr=^+aH=Z`K}#mjv+*~KE- z_Cu|u#ffRPTzv-7{^d%M(jzS)!rV#!eBl z`ni2NqDsb=IYQ4_*585f$l5GEEocEUPGu8O*@Jc^0LFEw1;cUE)*g)ga>}9T$}ioCFKW82f#psBc_i25PVKR52&}=CK)wHc8)S z%Cl%`Cl|tE=rr-E@HEIc74L}3mW_p#X#ZKWT~xQRL_-K{y&>k*b2p;Uu(m?a8I=~< zm!Oz0K*n*j7nRlP2WzS5b4Ap4ck#Cfg_TRilt#oO8fCn{QJzJO-ra|ANV+9H{o@@6 z$T)+45QXiT1&c|4y@#mGJuM5NpskG+GfF;}j;OS1O^!T^mVWI-7!(wXZ*x5$^O|jd;EgpA+92uLT)LD;AY4OoUba=l*t4$<9t*gwB`s#cbTgZz0NUixqla+guMq z%66jKEJKiidS{Bly3K@TrO!Aj>S(;hfe^|XDQ0tJEuvk)YeLVnznX@Sa;*{H_H5BW z##x*o3OiW@>&pJIyQpGhM;k&Zv74BT`$Q?Co$Hqbq35N%^QDFm@=O=s+Pvui8E5qd zQP|zv8mwzy$#7A_j`z+Wq-Jgxlj%Coi>SBt10f%_pRPa%W!x6u>RbRBsAPmF%-sxD z_VU`B$)bQgV@eTPV;YIcv`a_yn?G8}mu?@QK?v;?-wJ=xK*o6xD=M3q0V{Lnw`(M( zxA4bR2(dNgVlLggXo!A}Uls`Ya;1e0p;I(od`d3TK*s6ZRaAC036|zenm$5IZcfoE zgqSu^OlII?n8A@7LOy-f(Th;gF0>KfR)Y*Qbfl=Pem_`T+1`a>YIR$bBGl3+h`DSp zfhpK_302pX?nH=ahp&ildtD&oJnktf>%np6(vHL3VqV9#Tt>*{?i6!rn*?*1ks?&x z+v$f0ks(XPw|8A2j`cLE^SwyT+9HXF|M=VI&MwJw5LWE>Kz-iEpP9@O5-W8&M~GLUf`KgNh^ z_~Ye6zwdt*bLrM+D7X6lVfZ?jh3EZNLe-1;W^FcV?fg#h?RXN%INrnMqOzNn_#J6U zE0&6h*n7NQiZEFlE2eVG0n=DpSEweZ;ww;F>pl?Qb`1p?$D7(pR5suU{)cRbH)V=h zu%I4&1_#b*Xar6=~hlGK-2%FMR z?P4a~`@=j6b_vz2aMZSosFhj6#kX(vgACNUv8b$dss{R#GyUVUb}@n223ZK7f@NYV zqaMRNyzRsfa(}ft3qEKL4Zm{&8ftQ|p7^#Y4P>D8^+jdnhdrSGcX^u`k{nL`Qi?Dd zwouF@@AO5O$n<3KhfcoMsniJo9?#3!t1AALwe_Wv4u_`qexd2Wfdh~KI=WfIFWT=d zDw(-5E&acx7w3ERPRI;c4>HjHo1(Jh)h>{!k{oj4)?P$7y;w)gq|Qq)lja-5FUs3< zZdOv(k=%JN_8GK!QN7Q19Bp&5Rrx3Krynw75`I3}CU)WHJ)0EYe|`6zwCfuFR&5qi zy(1t4B^?))<*hjbdQ!!O`Y{s4Vn0pO5LV}s#Y_g=fr)7E$BGV=*JEH@&y9sS$$3&U zcV_wcSC*Z4Z}(dn_|wep;@ellKn7a%j;PFjs}xV8t>15uXyvZg;Ss`XQZF%;rb#f9 zwM&J;CN}nt*BB5Z4n2P%a4W@GOrjSumT6jSQ>UU367W1hFs>b!h zQ(8$BsCso?1tR0?OJXVsx6M{fM+q4vXN(JEpwE_x(uzUV$(X;t&y7qmSZ2K$lbnc< zhB0C)b$)@lyuC}vC^3C5gAC*vB}$8{099{a>gW+-s7r%VZHSch0#Oh&|b$Fvr6Ioc2A^73)JkU{d!WrGZ)_0ANP z)msUwSz4cZpNJu1b`GBOG9qXHC^4BiLt!p253~yz#9f>XGScF^qOv(N;Rjvb-R!6s zAh!1`8zRWFWxJTn&Lo)2{E0$_`EC`+NUJx9%4WX^zo=~HcPAyns&39KLqwHL6tkI7 z0CTzcoseNBl!A;@GD1|Axf=eFGbw&bl|)cN{nc46MAqREVm6~5Lod#o(g*(q8L4lq zC~dI^dQkqDhyRoa$-J?+^UH{^Nw13OY&Kmr{YJOlY)Ndpa zkUVEiVg({i`=yPT&aTNYmzT#08D`FTkdcx;7p1-W8FZrg`}c0TE8(`Mcx^T!Z+$N@ zpJ|g}E{{JDGR)DLAR|qlDoR_F58WuE-;8mE5>Bl*cYF&5$UdDS=JVPkm`naCA;XM# z9b}~RtD>~Vok0P}&W~RC~-fmG@N+%a6Km}czjm!)~ zBX6PxV?0`uAZ9dbGR&pql8|wVogf3*ZfAA4kTAlH_1QJK5lDg=cx-kLdmTqC#Cn!QbNr*s%+>}I!^)3^edil>>7ffmjL z8OSxSl_+i0bp(r62u<7A%HMtRq!T?$~J|4zRvCe`Ch7{IJi$aMF;43KfO^G8K# z10ErGl#x9){`>kPBv>gs&&?}+83wu>e@V>h%6^zizfD3$YTyMKsB;fd+UJ$19kSiI zux8WAObM8KMBLz6`(P+&%BRm@9XJm{^?_FcarqAwz9=-~<^bsi`P!!)gr_BA3&)y?1}$y>2-%nRf9l zFQ(O;Ud>j|T%Qf&WsKeY4>7UAJ}{HX-wPQk`TmC>_n98QF7Gv%wEK_kZL;RH*zlShhL zWUPhTK*sUx6}7$U1%-{KX@FA$G(;Om*_zg3auW(*9(<5h;gVFbB_yPlTdD`M~ucgWM9e9iIdXp_kv^ zDavr)KLc~fnjjP%GMnaujL|z&l=e*;tb@{xuY{sUn=uZM z0ZK-Q(pt@Pz!GTH`{R;DF~^-S0gc0^(Pd5v$N+s~MQJ$~T(AIgu3z3wl=DF%OyJHq zp{R1KlM57acNU7$l2_P3>ic#`lN?b`op>jro_9uqP;}Xp2#UC&pNrD+dhY|N&hh1; zk)ovUCnM_RPZf$X?$;_okybWUlxCk;3R2v@wG$hOni_9)AayjYxa1av#co4X^UUlj6qWAH(m;`P`Q1!WTI@`as+>uy zr)G-MGMj0LW|KAxMW;`8jRi%Lt0-BNwj>^;CQrpnyS9t!cFsZ+bB?JJic*DZ(m@gA zI$SPFyZbJJK^3KlZmV_=+T#}GrL1rwdab=76t$Lg&_I!+&1)q}bDu*nXiJ|;L@`g| z^0Riaf?oNETKgydAQZhmp9zW_?fg+u+wlwpfo}gC*LRfz(J6cEp`8h04dY*TAWCKT zX(bfR9wughA||nisI5s7C~&R@VxQB#-^++Xw*2^Ucf~TM3`LasXroY6%eyrc6e-$~ zaiX?y{Xl_peJUd54?FyXXp?bmMX&K@#Z$zIbN(~)FecsBXM~W?PyO64G}^~$3<;>wjgjcxmf;SztnD9DWZfY z=|S(h-WrLC>`q2le9z+I}z9VX@dR;@nXy`Qg(+{7oNklYo482g?)h-tF z%~(Va2LOy+`{IczF$4Op5|KgP_M*7K9t4akkbyLjLKen*N} zR=s{OnDhGjm0jZRA0M}{PGPxQOoUqgXhS5BR`j!|ZEhI?McTD-@{~_rnfptc3r291 z?q0m?q*z(`i3%9R<;p<*kdK$&>UDO6Tg-^g_H`fTqGkON2{dG>sBOS{1dP_5mFLB@ zY|?!9>UwN~?y1M*8zqF(?9@LKJ$u6rr^3mzz zQ%8 z0Fmo(rUW47=>wFQFN(`SU-l=-2nf8J{*?cC1^nkYDca)+406 z-qM3(Mn6327P5c|{T!h1TD?KkcIq(#M6MQ@5~!5z8yokG`+oW0lC_K-h9}z$HLhg8nsG@aZ7-x^$Z(ABfE3SiK$iPLKf2c&^}ODeH|-m+tU{TBG2kP zi9WQW#DUt`nQ`_A_ z7PM&?D4f2mD{A|B1p-92K6w(2Xwy~?g61t-SyWv2u3N~OGUrYLg;Cj5QCspF2LeOc zi`^29$o^`627+SCUN^YusT3g#JGu7^D10Vg6~+CUfxuAqbhkt$iYe}&fq;1*9eHop zS5;zhw6qf_e6An2i`qt|BQR9D#x2o_%8Sz+2#l8BvGJ*JUR&!F1FiwqPCp}L4mp6%q|qIQbt#1K%b1HtuwB6 zO%b9$ogAD23KLg{WKr9kAsQ$k|7~X%ids3hjyRx0_PP$QU19n$J*DnU2PizeTVh0Q zvoB}}2vy9r3q`NY&AI-bh%RT@%yT_k3DGlp<%7b*J8!(GE%Rd!0z&Cc?Ltv3uh&`) zHSkniD{8VQS%{t!(<~hn9tV$#;%<8p5K2ET6xHk>9I&BULx-%nksw4q&2H`lg+=07 zQCy&ZiqWIv66I*l>PPTte%g<_y4s~WL}%A#fWqNOTT$Gup$G^)dQGAojhLS0#j~`s zS?#CRk(v8R%BJ`;A9Z}qZbxs6=Chrn5gFUA|bKwuJ$(i>gFz$%}0uMXM=YY4B3Otw%(k&qT=4`@mFAkg~A1 zf@7LJS;0i!1r$^g78Vq}Bq#tN*RtLWfx^=1Fa!{O2{eGaRr^edrUlo9N^>x4 zm9S4z_j*WT5O$RaJYL3`dPiD-WE&FWrk94-W@_Ebi6l^h)U$=2~Hn|3< zz;(n|^*M5^%Z7K~p;dZiw1pYvir>hQ9)G{AU`BKeNaz1ZAu>;NUc3^DKya`34G{lj z=2#@}?XCDhJU%?fVT3@z_OXey{M-vGp}f^sGU%xq8!)(j-MiPg4sqkoF{}F%s`9rN z59paXyG8j^=+tkC^O#3spd&3l&?u%|tUuN`fB#c{%lG752K!b~LZFda7MRu{T8aej zq4p_{C9C=c+*&}AQro2dic))ijCVBQqb32+==CO%OP(i6yI!8ENt*d~EUOevc0zD5 zb~Z1`P}_9Hep^}jC3>F5LtqILecc?sP6<6r zb070BAxSHwtW}(#MSI*0{heD{m>cAieWQ2#F2bDM1M10<01j@MFk{dWRC^)r&9>5F zB$}ddT{R_@ltFFD;ehgJoZul5&}h6z@GllR_R%ZikSEVKe)}T=QBiW?fCu%CJ&M`= zYAeS#{`E;0flBiIg9UII!qr{~U~n(Umw>_lOYwKE!=|c8`j_8wG?>d^_JbX6t)A8> zCiipJ^VJ*LG{8cMninfL_Nxv7;Z(zm{1(ic(YUEga&TRVen49;NWJVuS;m>mwo;z=nHxycf@-jblO!f!Sm+VJ5zOVCY2vnU?+Yts}!&kQz z%4^#|>x+D$fk3sOgFK;iWlbhjX>I;8?gb`jsZ4bZl7T);TN!HY zI&lHrdMTV`89Iau+Y7BQIL)PF5^V_E^ncdc3&+1l<0Rgoc|5aE!3V{645FC~2*ha_ z3pqfuZ|VgRRd}i&+=Jl`CaU^@6K-v@cIQnlrYglzW-^M^qW#E>-ZXPQ7a z3%z5~UfguwRxH}uN3VirP5>Jd?1HVm81BEV*f@dSm$3{NEI5cZKcRoQm!ji={gmLk z9~_DrIEa{+h}}*1Q1im!{}E_0w*rE=n%T?d%(2>4)Hb>}fgCt(14-!U-lPqU{b)4f z{}|7GwCIZysh>@;(Y_aAp7In&j@+Q`*n`R0&0uJoe@DLuKs^Fcvc-)Pp5z;S>PZq| zzHSeN!v#p{fcPKdDK>ai!<_#@G!Y1AcO?+YO)X{mn*JQj6BtbnR;8+q3?>4-seIdU^!b%wZRUrGPlHmN1p$V@;V(*U^9HX==_TYmO0A^a(Q^jcv;cqygtIZDifi+$MUd)j?gplC+Hd zGzdp*liA+Mvz$7YQ*5Z0hCA1Hq`^7CG4>v4xAL%hzC^TbtKIFxlc00gv2L8Qr}XSM zjZiaK*_in@{6#h56n7ma>RWD@j|XfnkDrnjSzckt$cUF|y1!gxF>&8d`6}t;q%Z&S z@zhh^cq?q~eaFphx`Jb1fR(LiQ}~cqaxU4%BKbypoqCvg;Q}pK@QwM+f)d1*kFmpP zHW#;CO~#l1=V-HpBxJM#+Q#^H3(oSP3TeORj6h?4#F>)YZ z)pgHKes}9#j{HG$yuX_w%+f)|J+E*rzoVUZTj>Uy9fp9OIqy0DR?0GIxA|bX;lU>y zu6i57KtZ^+bOj?D2vq)iWzXr(wgn}S1*_WM-28ohpwIZ|rLPk~!n#FYrwi)}*dsm% z8DMK$Okiy*Ly|Z$5zzOV4`y4p^@bUqbSrE09~EG^%C_6E7fFgfx^=lM9wkY~p!~L9 z2i2dAA6FYClb!HI585SI>BKMO$hc-f1LbLC58H3m2&BO=JDyg*h{BrmXi{d$fysC_ zst4*O0BJOMA;s&Q^t>SNg1r!dGGDw{nV;j!4kALX_qRhDra#e~=c0o|K5M$tqjAh+ zDT%E4>m;964=vKY*v%9QZHN~^4W4!IFw*+dNGM-k5B36T>NiiG9K4^$g?rEJV`1)J zOocZZiL)loVWjRKf}z#^y^7mPe|REv9O3nOTa|M>e`veps;*mg90xBI@Kkp>1_q@B zSb*I{PVkg}cE5*vSvY9y_zj=3bsQHO9gY(Oonub-CpsbmQ>NeusGT^`@AhN!&QK@F z=QL?E2Oir7B8gER?WJy=ZRPAm7+k&j7#B^Mv*+8F&cr*&`eDg%)1bQz!K_kGY#vI)$=AdYYQu! z;`In875=80(o{?Y#jwJ|2Lf_y%-zl!s1ZuTCTbsw>Z3@{(b?R^eQzxc)8C2^u&TL^ zi`FaWFGOSZCSIit)2b^hAJByiFQh6%uxj$P+(BTk{dTX${puQ4hX5l@hYxbR_XUoA zaGxi;67&qu@M}eQrD!GR&XN3^!9$LQse>BIz_G%X{0;=D1K5Ptq1p`_y15D8>4$OUjgQG1Yj`{xC$=8eL#9;OPDnlTgl6 zk~QC!gT>_gFzzR}5B3}l|4loy+_M^Je=9lqO{|3>sQ<(bKWF#LY83i7AURe8QgD>TpCh`Q2_r4zJ%^iK;J^g_j*IxIw5JboP3QC_z31YVsU@ z{zTzy4h`dMCZ$q6=w*(SU7rt7{42T((UPO@Lp_me50$oCW${Clp)HApldRW@&7$yt ztJ^TFx{X5eF_umLuiFtu%@XV?`1PNUpq*W%z@*{94`AmjI@FG41h4a395wHPBw29N&iwg z!^7%oPjt^}BBX6G;6Munm8wOcPUqD>X7M)&sk2ihtuY;knO-Rd^iOPsVI?&fAu9gJ za2SL&Qxm5p8twxm=XJxDofD!-$ADHQ9w zO&+$A5rQ#om^EjGVl;2Su z8G^M|A6ojLG`L6%Ee;$`K`or%hw(RyS zhPK5Ztvu$yE+>3SZ}B(oS%!!yjP3o2&9yv$5*~myD{{M1Ue*&KZ5j@?f^V3ZS@TvN zABpclE}7`-oSJE=o{fY|!L0Q*4uswY&c?8?!@p*R$hxa>FZ=O9adM4<>M#~_Sjo<9&7+=(gacA@RfuRb;O!C?k#yG9kP@#macKn?K&naLNsw;RLB;-V5X{3ZHpEj{r z>vCT>z6<=rTT1~G{l1KsDgq=*7bBpmxi5!V%d_dGikT#78M~7Njuw3L)L!}~tkq?D zU=@z?rDys37G0abo+6WurZ!2?3gx*5k@?yek{gZ0ozcHOdf*}`s&4#AR@c5qa}lR9 zgaN2 z1`wR^YAruXD^TWF|2X&SlFFyc+R@sc>j$KT1zc>lge1A07h07aUiR+~<*2_F{b%m;dS7ph6CguYJ)@m<>szmQi02x zC1~EKjzJWEEgQ<*=b_OgSc@vy07b{=aQ*Mru|&^PEtX0e{zI!w!>(XBIT@cjm@AO@ z(DBS!p~$fm&!Dvltc%KRf4j&iUx5qk$q9b}h^zYMc*SF3m%i5?lDAwX{U zCzDb7wx@c%xIGZQBd;~c&f&Wj*0oE&A zLqhAWs>qH0{^8{(nogMG@UJ2cvv}jG%MEH?QN)Q%=T>odA{vt4lU4jxvL{0FsO76$ z8_Z&!K}I;x$_cYJk`CmIpc6A_O5&&Y!3i8ITlB0ACRhp$9f!yxDja{F`t#*4-Ih{l zSX4$?SW6zz5Q)8{qDoO9fLXue%$R-;n)P8=R}sq^jan!@!)9*+}l8)=Brnb0rg zw-$j~{a7XQ1F^kvZI=)}-TiQ!phFu2pt{5a6cwV)+v=0_ehqkep&7+{^}1!2N=Xj= z(NQA8zQidQ9HP9{@fmuth)eK2I(ZrGaIouZ?PCF~HYkALfw{l6f9pn$Ee4la(9lQf z&-8?ZeMrD519)~OS4ZL8jl1K;%HFHtb<0AsJ{hKKB56=QWG{q~ZwDceTNXK2z``06y6N+;&U@ zs!K7ozPcvy{3-j9F$PFvr?(0K;UC0=p_K{bW0X2y+pIrA9=YTJCm|?PPlO3>hw!^NV^vY zg~mZ7v%7V$;m>wqE%N#gW*heK^H&QpM$FHKz8_S;dw%>G4I5yNq24znBkY@6Ipqd( zF^Zf?5N}la>w(uC=+ARMKeq%xnk3}pQ90ldwtOVX_}fCEDC1YwuPcIi|F;3}X=tJN z3GuOH@RZ}>AwB-jwg)=ViOsy*u=eC$EDC);dHLg)R2B#n^PRmdEwPTcD!AvN6xVFq zGt^LS$ejWD%#WxSg7&-^WW}+Spl0(bVD=R=l9timaXnb`o#r?I`Er@XFa!pVd*|8BqRTz)O|&OeUg$?c$MM zLQg3SK+(I;f5Q<;ve#mR5h`wNom$|MK+f~LS64lRE$#S(s!{qnLSTvJ@`*3R@Nl+< zV)%HKOCA}uFz`hQJu%_Hz`-gR(Bnf>D-|$zq1hn~e?aPl^*rzVnq3Mg+A z59&=S-dfT@pA|X6X~8Y6pOiXC-kp1&;6_)Fe9s(>l2Y`s1hk)`5B0|+&RcSA*c{96 z_aM`8&5{4`8GJ}2kc}Kl6>qP(wX`BlDcTVwW&M0Myy3lHHft&D*KQq0qcX{M5!6FF zVv65=bGBJj0*;-Sp6BfC+YTxUl8f-h&XiCAFd? z`{{qS3Vg<1uPc_5uQGGrigjK`T_;W(m&wCDzEz-_#DNm=F+a^XPGxoyG288Cp4W!U z@M`hb=O3&pJlN>)fE~_%u2$x?%1tKG*8{QZ#fcZ7JR009rT{3kG-vRPrv4d^2q=HO zs6ef6mYguY;=P__xNhla93i?P{Oi|P01o?1siP@P$HMn;;r!{I!0V8z`gcpyi6^=h8Vl*x6MrO^oco#Ij0Diap$?oK*ffS z283j8WEeVAVlCk7&LS7F33cRcl3Pj$d6}EPfQN0ZX2YMMj=9SfQk)}VUkMqVIpKv- z0ao)nM$@wOtoX@%=P|$nCp%KYt_1W2xn;NS(F)r0b+$ka@yun+HXI(YkS5|?^LP?P ziUTVblWmGKBmU%0geAvOYYB)zTPj)uF&p-J92%h74svAR7lv<*d!wR*xC`cwS37;q z0;4aw$uZE;$_kKeEhQDzG_j9y^#w+qGa(T0d?l50IEG^4`v>0ZmD*Gf2g6P+;)W6| zs^C;ncF%M8D(JODNPh?d54@ z#6;3+VC-3U&R4V%UK3chP5nJ@?uELq*S23rar($+Z0@oVwse$GP6`JlMfAk@~e4;SM>e9b; zAu76G^V0E@i$T9P`sQ;hKB0!9uFtsF&UM^WTLZZ^kF~zXUZB-(zPaw#o)6h#Zf7OW zb$#Q?MFl+-zQLoWN_n+FtA$FJZ9(z-B3BF7ctL!rBfOrmJB$oOgz;w!R5vCo#P@lF z=pbRlf*2MdilTWxTrXzG#HO2~C_83FzP0PARKv|(fBu`u-K>_>d3 zU3B!l?HX`=7;TymO?V%t?zAZJw&CvK_-<-!~o31LK} z+FtD6XQeX(Mbu&JqhT$UGP;4V8mJWt_hxvz-}~1L0~!zz-DJy4mCOddBcug~kRDQ< zm8M1JYCq|oX3#N9tqj3^l=Ns_E>E8ch=N;w(IN1~>r{rewtclm2R36+*VyJ#E2nuM zeP;hkx)M9S*i?d}3tYi=+v);2Q>D4NKC7SLyMMZq1*aPSD<&z!7zOLUjzcKsVDtrP0VEQiz{Jb3jONo9+W^QgXDfIwoS}4Z@ng+R0{dFWL!9#rp&VN1) zWUarl6=1sS8q8fTioefm*Iz4nV=l?}$RBj{O5g=Jra1rzgmr!hjE!6n|x9*f%#6yq=MNcr&O+T^UloWVpuWO>;VO9peU>r| zt+_PN$_GQKy;9eo(NDjg2F(LrspN$B8o$zPC0C5jS^5Ol+4j{G$N`>|0Lq?S(qvy8 zbaj>o`<=+#A9Tiw;_oDlGSk|MR8r(`^WRH>BURBtOT`T1N!&VBlp3VYVGD}(nFxcV zkf;L3*@{ANmB)9AU{uu3Hf)yLLvF3ZxiRPz@cE}^LH7in}BfxPQ;$~ zRHD@?3rSDTpCSKr?np2fixdk|p^De5*WbAUUZVZ){f#a*ZFUjUHf!LRI^W~^u4qfc zvuNmL8=B_gnVzMLpM*WWi0@U^{YuWTh!kWE=5-SF(q@GZja;tok%PM#A=r2Q6^Dvz z3h?rj&RPaoj2nZ=_(XGf0=3%DuIDzsEcGC?5rPKZz*p{t+38w%w6FyG>(i(r1^=f{ zVN*In#y=FhmooSKnyA9a0~Tq4iuRN};j<&kC1dy00;jE3cq_W&r}{cmN zjXE>Wb1G2+R%Ii%(^m6@y*whh$wJcJksnl&pRqd5G>@abiD<%0`yvGQ$6<2H-Ry+Xf{vU) zmq5h}&#M&r!~l0dc9Gc~c#78=lgC6XvB4-GHo(UWf7Y88d&6zCC5IYYH&A46Zi;mH zpJhrvI`)NB%2w~UVn1v^&F~EW*lCArY}rW`PT$@vH&pBpi%s=$HcubSd8HjT?j?#p zxg5qrd(!6E&KJFklHpn+TJ~*y9gTJHoNe4r#Y-TkwH%0B?_8+zj^!`=-n4ZdeWq`J z*jDPp_!*@(y@+|NETa>5pUL>IXqAG6;vWBjG&bUr^Z(4~E0yA=g4o_9XP;$yXY_4v2{f}03av~N5NkI&2vSu zQCkB|&rp`sjBp42V)-w)7WMa+25^yjYemk-|710Yr_KWzsFhtRx5YTseXy`Iu@X;-zK9;dfJzRmE33+OwSw66?$~CBmH=fk)!94MxxZ zDWfEpi{BIDcy6>A9$9oLkWF1%B9z!pM#Q<{vnQT1gVB?B&FQrNs!6+rKFZQs^?Lr5 z!>w)5q&chYJjA4fbf})MysIXe{8VAl(arl_5nF$i&SeAvwCY7y-1G{4Gksl>fM=aq z`#ap-=-T|HLt^`eLufkXLVD>6T{2B{?aK;Wx;_t({Ob}g07|ULaUXp_lJ^nVA$Ozc zi1$65XlbZRsO_kU%IboVNbs|l*9$b*9ROPU!E8z~zNI?6@z z2;Rynxwy>a7+m&Pd#&1yIP+6tP(YdryR-+50Hv-QVHb_+27D-i;8P}ZgArXRX7{GWV!LZXwHz_8j1(@ zJEh0vT2})pRhvl!-zZA9N)@D4E)83sMEuugf-5VP8t~`q@}P2m!BlrzLVQf#ey2Ge zU??@^dKs3}Oy>25!+PN9N6@g6mLO>~fek4Z-N2ZAg49}Tn$TenM=YOvT>!db$MR~@9DQF+Fvb-Jf5^D+PZ5#ePCaoore5>E zqYlkCGXy{X+el>%Jz9x7O&NNBhRIVqO}0uD_p}PRzPP;AY*jtaZ?lO$?r*rJ6!;s@ zYksY0?D=@uUCt0MA(CG+&pY`nJ3iOW>}3qWgm@TjJ6lQUx|q6BLPpU zO-{}n+A;I(@5PrVZc8npUMHP$BCh{7JF_lzqIP-*orh>-=?%bQj(!M&fNVm+ng4)j zw`zxFRW!S!yBFf%zAALRrNS`eTxfT3oJ%X?qYpvkRkelhj`oHQyvxERyWLcp=J=A| zjWWf7xCN(=X2*lVQrn+va=9#b9hoZ7$7-gGa+64iO}D$g$|rA--VS!Tn~TYt=QE8W zD-8HDx6|9(`=MG`wV$&6WnkR$=L3%C?x)I$KLtsfQXnZym#bur*V3z$)Qx*&(u)qY zC8qhr4_5P1eE#T5nRSRgid=BB<;<%;1pcc?k~vT!F-!+zwzy;bES=$g{w9#Yx*lzZ zhiA%|-;V^jG|}Xg)QGY8`it#9!dl_f4kiJv7W$db9JM4K;j_I@kG2m#X$B#rC4Qw^ zp|#S?Ki#Nf=`Q|Y&<#zvCEWAaL=Xk}o@+E;!v!cM704>{kGv{5d`HPX82DgVKv2=GwR)H=D=Ch9+_*`bQD8?#>>%F z={7;i?QtKMrqov%IQS%wf_dXTa(LZiF&`7!@7_9|IxS%YD!#!1Kwj~^#=s+G_YvlL z!^m!^jR=KSYL_4Xa22`6qc%lw%X2GnbpwJ3 z2QIPx_E;bKa(Dltu6-c#066X$s%DlI%siBv40%fWFPZ9QTaJ%MEOdjh4oGu>McROq zI|#gZeqEhD@$#N8uR=62L6=wDsr%1AQLABmSLxHk3s#24y_GFh*P?VN!1wvepHc-% zgLv_|CIPWN{P;6{_{)qa>?mi0yg{JbjQ({(I<@CLi<=6c9p1l2FFen9nFUcne8gaa zeO%s}Xt|lgn|5aD&t|n7s=m(4F@cLfFRQK<8CvAT3Ye_qY z2z}29GNbw}AA~+dQ{s3&N2am?+o-2I!OD$9m3^-iPHYk1~&Yi|Zw1*+}2) zGXKCok(_f&EPJFk<$A}cnyo(nvDZnJVLV8E2W776+VY>6_1PSBc}!8L1vAN%@X3;R zAv7qrUxw^}Rq{@GulBnzq$3U=%K-HARsT%heC4w00HhRZpU>M%rA5(YTPhFW(4{st_oGD=;#D}D`H{Qt1bla?*rASe$@a3?qRAZ-C zFA_)A73mj(QW7x0Wh{G{X9AM!!}%#&&-h#R?k5CS1+Rd8joNIWJ%9QG5!SdBML>ep z@9=iKu2kG1YAu+KZ3pAmc{uaw`mkABi{Abq=BJQET+4P!m1$wv4_9$Z1$?+m?vA;nSO*=d0JcYIpC6# z{0KP=*``I;7yOW5dHTigM(Z9tZ{AGSy8dn~c|{fU$2K#-Ff0f3dLE-HrRHC}{{+L( zNw3>l_GZHFfya@gD{UpAHTZA@hxBG6Hv`m=XN6B8((WY!%m+^z1d5O1@GE z7R%TVtYVfe@~o#YWiJ>Dy6hTUa!j;}`8J=tMDs?;8n>T&RKWbwGN>7YG3i2CR6cx< z2xIvWeveqE9R=3C(~QAw?U|}~ZXiVHmQM8$T)$d#meJs&9kgMpz9M_nf*PB(h%Srm^aq`bT8-y>5`SXr3z3AckjCR7i7^IB>Ggk<&F# zQ=m>j6PsDxEWtSpAC!c zN2@Ei+Oyx$Ez!jO$ZrxP32wh0d65nq9=6J zIVo6e5Apxi=Op6!W%XyTvau_7{=AHNR~5O(O0fkbxIozY(mX&msPbsV)u+tpogAi* z2SnWIWd5<5OVH|#>av;-Mx6k+FCOE7DQE5-2ou<k})Wlu;GW+`=Gl?ixT4*;h3Xac> zNoup6%fJd-`DJ=9b|H>_BD9$%p16Wf_BC!4YdbE4Au&WgJ7coRl!&{YPSPdr^L?Vo z`OU5Gjr#hR8jb}jQ76=N`F;6_C`RYkJ-D^*G`>^|;xN4Jr1F<1qcO?4ZYZw09L;Wv z0m1UKb?3hf=(^dgve>Mw(r{iaTmxC-RVIdp8!gpJy<&Mx@o$=BDrdt3d|7|Fg|N0l z>lCfe{MnC7a+nqfZu}$~yZ`${hAI}8>Tp)38Jjq=4aQDX?$Fqw&>~(ncw0j`09I0yFH0TJI4oG< z$X{_CmtY8IoNm@8$_4yl@3Va?w_FQ!O(B@+mEV~?nIqYPcgxOWBlhqzCvy>q;oYJ1 zvi20yI_@hA{K8MX0Au7AiTzn#>clF$A&H1}HsRf4f~B@jv6fXi*mnx4{)jIqC_kn* zRhg1A`N_HlC`&iMIBv*kbH4gPPfkLOPT*WDNmht<1Ye3t;@E-j3s7nV$!{&(LGOL8 z8h;NuDF|`b@sY1*{%1sXrCE=$lag-m4(kz5;2G5>U-H&Q7K5~dbu&Op&x%fvFda+k zD^uJ(=<*j(PNJVL=(%TDZk5Ee2_Q#F>0Fa0@jlrsO(&>*|NNT`0~*%RRbYb)F7OUo z9!oJo0V&$aRcz^wXm@HE_FLv>h>Wjs1V!e!K;ULd3*9uYld}$U0b`EDL@DdSY!azi z@+d~u%bY*hJKSnLVgS$Bi;Za$9yy8JS!pLc-q}n^Km}0= zA<7&!C`t-xLY1u#X6LxA#g)*~ut{|t=DV)=Y_)2YEb8O=tLqGl&urpw z79*W2s*a!;)>v;G>@KgcE-G1*vEQI*arGbO4fe*ul+pIP@U`O2t!z`Iti!;uMT4Bg zc^S>WnQy-_5}4VP@w@q@#Xqfy?(=G=`5o$D-;>pH8#yVFfuVta7NoB1Hg-)&lsNq9 zeUP7>dfo+g75rzsPFTB%z4NtFycF!2(Oh+b4 zQANf?{Gezd9BW+`OsUhB5Pw$5`2JvbWRXp#h8!Nb`adEsuAkpU;$P8dmpKt;Nwjxx zL(Wh3%~)yPD$!rjC#qi)EswRc`mPg>{XW1Nx548b9AoW>;gvMu!5*RhUN{spZq2X! zM4_MV(P-lcs{yt|opZsJmZ~wCjf^i)v^sNSD$_g%SHw)+&RM{{=7_>@M9W?@Tp;g{bkpM$uV96ETUBmMP;&tzrXZTYys04h_}x9I$J* zUp+bgr)eDTwQBabx0t~NsXx045DzE@!3z5HL`+)hP%VK03C6z>j z|7L$lU^zH`-VaX$F7p>$;FcVstQ&)~PJD9;s9`PB<_nVh4O|ydX5j&7j*H_JQPk6bq^*p^1j}}?rRs2E&4{@u+Ffim@n}k?kgm@?-n2WTc{n~Zy+YV>Who55se&Tr zfj#GV&3@AnhZ>A-nz)6(F9L%<>)>LgeKSIe!tbZZW{^VvI`gb{JbL%qAYtqgZ)B!f zU1nDYPA@ZgvQ}-iLmd-`Fs#|R@pvm=DIX@*sEv~JIBF&{GadA78WK3iINq&i$ps=> z{vJWN&1>zwdZ*sHa@GkOz?EKg6-}R@m@Lh)7v%4!Ole2Qgg;>ZZcL~N4u_Uot#!a; zW`x(hH*MqQ{70y4Y%?T8DU;gs3l42g`B!d@RV5JtKoV9%*KWQQr zxuKGM@7C5FZI9auI-d!e#?M24Lrqu((3VJj9iz8<;Iq>x!}h!kRQrrCX4ZML-qj4XQBNb(XuVONj_~ihHQ6*E(@=BEL!OERg@JI4q2|9O5WPrfym5Qys z=j+^37*ZKjt>$o14m$_3tW1;Ro1|CrsvJK!EkEml3ieDwl;vL}%Jw8m(zV34ojtwO zgF~XT4atE{m@=s&|60iw5=!`|9~r%@hLUY;4{^i%*ap5!SW^XHdKbnik|K}qMgt0^ zE<TNxzlYenuV_oaC^;F@fkl-OVu!f5U<-s$e>Q$ynozmG*C55EGv z-6e^|;pdvcVv#RD9;v%O53?gV^AW(B&ze5%qKYFi)2OI4@^><}QSX#coo8PRhjupWpL>_u>yQakFs$agxI8wQg zvbG*^#$e83iIoC3n}moB1glf-Z(HN;RkVk_5R6-nw~l_LxC3s9S2D}YxSS9_Dkb1J zk9z}-qy@9x^?;VOyKUBi+G$vB&kmAcFC;Q@4TROnJ`iAf=?d;zrfY;%S@LF!PHia^ z1|bT$TG#KJP-UCUt~(`6;tGsU3eu&E6NU0yl+L)sanfrVz4S(FxjRDC+k^%VYwGrU zmPdRu-i!Efhfqx-6!Z|om2)YE5#0C-1R|w6@0Aj|#9^I$BkJ9dMmQhr=^d-7E^#T= z$}91E0apRu*b6R~T5D}fJuH6FvQWKo=Rh7HF}`TR$yo>wRK^gue3 z$@SF;&c0D&N*O~jUZ?Q_6dP9XzjiWa^3NyK1CoLL&VZA#|GT@#bxi? z0#Vzg8>-v(N51M0^Im}mr2DMC3LgUW{v}r9EowX)&Ba-zcgvZ=xVm?v_vOF>+5ZoP8UY64)O^IBbfXmeFBXaXCZ@1q*_=f(5s9@kL<139 zoLLE<_r<$7HpnGU5j{(It2mP~-<@W@6UWRNiO6!q?Pld-6(eop{iSET0?uXj2{vow zaz^l(1YmWvQFtr?6nLh$o5W;409J1NQi7mCsc_alAk% zHq?fWwblHJp-z<7tcN*ejuuheni2Uqyq>MANRJc6L7`95BYsFyHdat>S8>wc`FDf8 zvh2s`j2dpmn=LX^o~()6j!3**|a?oft>j#aarX2qsKBETlQ#hUzux=Et0NrZZ4R9n~?mw597pJJ_Jkfwg~X! zZ~A9b@OudF-{hg6&oLLe3{G$7-$4RC-IBGi2-Nx7_6IL-=0?iKoH+i$XVw}YbhPd{ z6sfg3b$(5pL03(Fz76iYA;th%R|J2~_rgva=uT9FyBPy|?CDkaW=G8sgr9Dgy48Dy z%Ez7k8ASTFuIloq0`~k0_A`Qgb-n9NE6GK+bv83`FhJ}Bk)NsXLew}SP#l&SX_!6H zjaAZl%Ay5h1#d;~k;Bht;Bjb=eP#0;Tdr_J@~zHWC8b|@%Q|NQ>y!jDfSxac%n3`y z^5fLEK96#RHaq*z-Xc?CDsIbzmFo&n5Q+|TScEe{iiEc24DpbG52EpWd zYm1SP5|*ghLr>Jxe{ViK@y-^gAjeJ~U0eSj69!FrK2q<}XL;ON@?$iCa!K6gD46zJ#B+< zYL2Uw_1}4gs3lCX<)}<@fTZ89Wm{th5{^ZU*kgc2qPs`yi4BGSP$g=A1iS92(4R)b zm9cgR0+Eo(83D_LKV(>7kv{eJDSp_oiDE#yhAd28n-1DUwVs#wDhV~O#j|+dDyy7+ z1A&s(E-183vAIJnW)qROBCeck>B0VB8F{%(uJ_jw2-?A9!u&ii>!)b?}#53j|&vAThoihZ}Uj%cBN58Qq7G0}}XOk_6D z#uv`5g&CGB1*AJ5YQRWKqC!D*KG7n&?T$!f1wH02$6i(XtbL5lc+7rOV#P;bp;t;v z7J~_v&-u*;RsvvS8~ukxw{S9sm8O&flL6jvfEM02doAmfdObD>C}2%gMX@xo?tsJW zHOR>czB1Y=!0&gY_(!u_qd)gZ(YfwiV6Ri&j4|LRdvSFZwNrRjGN%Wf8@~N__Vvq{ zzI1+Yc*t`fbz*W(U+&*QhEj~r3{Ml-*zWqDEsq!Np&vs&%bZ#NgtVuukGHzUk=;1sULjn(Q`y>L;~L)uc?)mC+t2mJl7KXtpx`nU8o~*M-xU2raltS&PTwi@@0NlWLZM%cW zi)`dBigb=36aJgH-zLK|kYZT}x6W+j_O~8c%ugFD0dJAs_(qC;*g3FC^bQFiFUgc# z_TNmI#6Ba6r`C`%75e_Fy*C&N+QGan8~2i9FDrWX^kIpSgMa3h!l-BqJBQ{QTMpvp zDKWFrp#O@$Qsc5qtBSgbke52h@GN&Su{h!2ny)3MR>>ZhZrH9< z3cy2bwYh82WLgEN5!M%_zn`=|Yh#9ew6M16B=(>6#=3#y zEcs*6PWJm zMP`DV1LRI=?h!o;cB>;7SYWd{X>=Z>wuH_wQ$Zj04|92ZToJ`d(Lv14Hx}MAyshr? zgbV-mEpp{)`^p7_#^nH|0zJj!?X3qFBgE+0t8e)!M87}f5+lM(tD>(ZM<_yi>4oZI zOh`=Y+7!)^qh25n0tMucG&&>=NXIjmFsM3^q2^d7qT&}@h~}d2I#vHIgUUj+xXOIA z^EMp^Ij0|OnP<-j6aCK>LyKL-+6^cO_&OV}RF_EvIqt{1Nx9VMC7Hc_Qr^uk<0fw}2nh$V><;jlvdGjJkgX@>wiZ znGb))4}FGaL1nkB4i!c$+Zxj7KNi>iy-4O8_&7_*da8`vD)TvClVga=!pHxk>8it` z>b|aYcXvv6sYuPxN=kQ0w+tmEFu;JM(mhDGFmwutG(&fHch_&;?|Hue_r3Sz+Ux9n z?p~tL2|m>N(Z#Yh-+|vg-h^ZJv7cM9DlHcXu)6%A^}&K{Ce5tnU`pEu19~-(44g+$ z`3!myMwWE>s3q^K^<-vPfREPvjN>;SVuQ#jk$Gy4`u5#ePWRH0?&M-AC;|H)kjJhd zHrs_rGWk_{Q2H0SVn2nkk~z(H)S zt*>~d_v4fNxlZkqu|2r7xbNhr^+GrCoPaK~M_&`EW8S(Z zVM7!#O~DNA`%=|D{@4a-Zw$VDe0>qGwK+!>-|Bp|dyOi|<&Z(&Bgyb)B8r$K@ZA`4 zkMLzy7z4BzmHcB@LXt}yQRC+(C_sv60@1`4q}^%U=EAFs@a;P%YW=a)wV8kHDiOqA z{CmsL8R?mAvgo9NkFR*>(!X1&Rl z;q9BFDLBkfGwz4E^zZfS_l((juVp&p2=9Y1aOg_y)_4U^xJB+YCj5e$9Z%-$Ys<4;CDqXZZ*2S%U5s3?{&REXF)nwEmx2y17P#8WLnTHKz;ZtS1nZVLUDzSXonEkO>U^?%H5jl%J9QGvFJ& zG#5WAC68n|Kmj<6xjJ!hlAyJjik3g(qZ(29S%3qidsW^YTvQd%?r;@0guaXW@rokl z=Hvmlslat$?wXE}V7wY*K~RK*@^f@8Q`xtKPWBhOO%fvili*FA^f6IfY)_E8vCi7w zpDNv(%4I?)Fkk;}K>f&P1->WqfxtFWh+@+zPQ8^si@+&O89?`nx3j39AYSY9ZwjSl zV(#m=VUKecf2FO%=$+<#ssPqVVgm&QR=xjNyrnYS%3)Lx+q=rn=%cqFR781y&Ul?$ ziq4{Qi#|@F_BU_Qt=W+s4zZWI8PvgK^zXl3e@|X$c+7o(wb0v!m3ULe)1<$@;=Z?e zbtXa)h3cd@fo7h|rNLO1Sle0T$DN>+Ur3;|oKFoB54*C}l9}qan&5i4u2iGUMaDR9 z@snn6i2V7ZN+F=_>XR^G+%MYl?(1UiZ^6HFkPXtRka8Y%8H<#jh~r!P*~ik_GtEFk z@AnO@_rgr^xLDqXm7bktLrDT<8g4dP9U~1H>FqWI@alIOl*~2nP-=*4zp2mv>Ery- zj?K#dnMgd{d=~0AoBSnUaWRA)B}K&<_gUJld1%fR3iu?&4WjvjK#pxL#$I{tu)cE6I|-*PPQ1!G8+uK6}*=-I7`W)e?}Yl$Lil z`c(M2DkYpg5^xy7cDgsl6}?Z|P*NP*1>Bqs2V}#(xd6wK$&H9mdOYx|YONl;%6}q- zCLr}4ZlSII45BZ%0n-~50EtDgq$1Iw)t2- z_Iw7z2EQ6?2)hwU99=a*3=+ClRQ%jMT4zxUI~sztE@J&?aF+Cn{Iy=)Yb=g zvKx?oiJDYL?qAS=m@ulnWDvi#XC-`3E`UXuhzqbu z-|8%R_Sz&-?!f+0^rRe@Rb)Vm-1=KYCCkItV^Q|~H7)?6ZnSn}jUe-%Z#WNUL!~-9 zPfF<1{&EGYGfeiekCS7K-)Q&nr8bcHe7=mLlq@XYiy8T*X04&W)$QM&${ZTR%=*@- zC-$`~26{5`=NW1g|8o}vF}woAx9f{~Wovk#`)sB7KCDK;zG=NjMalwBhV9(9SESOS zz5tXyO9lcjf#wPM02_}Jq?~&x*{-69I|L$z7FFZWJ7Hu~g)->GVwEdT{pvQj!65f{ zmnj6lFn#V6M3M$%6Y!vO#v81nuTC5pv-;1!nMEWodcY>}HH1DzDsMer76-8E9%x0j z*cJsFL<=g4jpIwe(#-jP!8w*v7(Ax^33ERDt6E(jS^dZBr~&6+UqVdG`!F~)u9{mU z#ax%K)b^vRS=WwRkBt1U>ZeW|}Mrt_Uq z-qZyec{T2T?F`+t^DVs^Aa60PQj`;+B>gwFlEd-5r7_!jlY)puxS7i-M-z)HDCHb5 z2hKP4cQU%83uzIcss-4|cD+7p!)8@C%I)NvHNqP&fZ18!v=Tq?7#FCfO zT@Xn5jr?twJZR&w+&pEh7(>niKuSBqvjhSu1RCoo(hPDqg4J)flo~%bGNJTqNzFx= zR7(+IGT}c;D^5|FySdhWD+fKrbQVSBB)IjveL^3nxTEpc*}9nvxLNXUabCE{6c+Z#PJ4KGv-vrD=g#=X zQ_HSjFAIF@ofm?Qo$?2nsb@Su%Fx|c^w7?6obMuxb=SUM3Y+)NkCo{;rcEAg=LU#L z)H^($0k*&z!btfb@kBXRCbGhonPmzwJX8d zo1mnA0mwUu#0(*bq;}x+?Tjkd7t98PW~<7X42!EQoX(|2F5|5T9xNr&-2{hgt? zfL4~@j>3-gIYmVGC`DQ4kAOhRcv-Zhn6m>gq3U??CIN#DaOs!DcRQndT)X{4d=tR2>eDdg>)V^Y-oMmGGyWGJL-mNGQk}vA2sakxFF$?#hK3dSnkVFd< zDWTI8M2Und*}Dw~?W^CC0U-Q$CQ3V;r=1aANz{ejWM^DaQD z&{o{`)_yQ^sd}w|cHXA44_uf|J^7uol_t_^u?)s{YpICn*|wqhk-@E?)2vu(J=)GB zKx>ezn9M>&GN$Kjm9j#3JbASPi;2yK62^YyIa|HMd^EU>t$MU4-v(KKMXb2-f$Qxw z@rx?vnNBdkk4+^6Z`4W1Zeg(iKB>t6iF^H=l{LP=qnOc_22A*}6>;JZv;V3!TOEXY z)Z&Sp{-H8Ywd;_LQ8ZXHV}@ z=Tnwmscz||O@{4?l42FQRE$H0QV=Fa1{oKi%_>#%?gC!e$+U(YSM{{4Dnw&Imo@cK z>w9r-7zGgWQWum1P;~J}qLFG*dx*vTR@hqPP>iK{h8o%XzBV3vgg2aUJ;*al&DNp) z61X%?$24yv7FL-Zq#}uF_0L1;?ZUJsV1N(N>Vi>PhAd7A9du9io1tF;WWVWwrWeN-#8+C?eVKILH!35TmHR+!nW&=nKO{h|{IPT;8iYT{H3_K0^d;d)MQ zfcigw59!e6&TwgXd%!^K>mDu9J$Ul=j%JMtQUiIl5@-@Q7kFfY?Ov)pG zO~h0DZ|-1ZmuOskaUrU`Nxvy3Q+VMWB{}pYD%WKOd!9l(9aYsjP$`9^9Tt~RJ-<1Q zaDuE7OrJ6ysMx4la$VL9#j8tLK%Q)J-Xz)EX-fKjAMLW5XtC5GaejivrByN}pFBdZ z>7?*YcyE)B*<28zBxjnuOTI1n5EAB>W#;g~k>qNNT)@LPb#7T<#I8e^Q+8C()tDz& z@jfaO@uHT8EN)7ZYgc##Vg=IrkvDVEsjN9FfZt!bQm zo{i?cn>ayNL-N>fgk9N~f_abW@;o09KO19>ePQorZzFTEGn9GY5|h!h^7_=6Dt*L5 zY%n&+RzXvThouUssWcOgzU~Ug_-D70M-JEdRF~7$hCRB;l3-nVmpNS!CkrH5&vK^15XEh;3sz5q_Qio3jvp$5?UEeN{1GIt-!bmH1 zq@N{O>!}XxvfTI8mCS)WR2D*+h<(;vaD+E+(^^fOelFEt;KseO5D>q5Em41V#LmGK z9xzaSUr97MTqb6h;eLq?@~lYiDgeIznXoIhFOSCA65j~EJ;aml-cNe1k#ZGz(Tm9& z^PJdKFvQTnJZ7?|jONOqxK5n+5v%)P#CO5ZSF2c^-U7CfA1zQ-Tj+})37c|<7Ve9EYD~}+B|x?_1Rh|2AB;~0r{31&ta)wt@=w-@12LKl zfO<3>i^Y#fsNcqoU?ak>u_Bcr*8eEIME@ccbGNBrOq+#OR)%pb{%cLaM#^0Q8rrH2 z1A)Yr>+^)&BNMy~`%>957|W>`CX#-TF3+KB2Eb#tdy*;HM#f(yC19(;$mvDJD7g9Q z-)?8&Xl@ya2dA;^yZ|KV{`)xQQlFrb9Po3t-}kj0gj93$1fqVe_Y>W&!!s>*gJ6qU z*eyf1Y7)Wk*Te~gF%$l{(pFkrlO}CeMGdlSIHhh|13EO$!=7G`Ij$))F!{D3A^XoVko zSlO+qKJrmGD-3g<>W+CgoLaoP3I&uT$}YD?{!A8gnj>fMJ7PKp!DAWac1oC@s}{31vK46G zr41@!&6Sc%B*ZZ^F{bbSYauw68ViJDWCi1;LWL)<*qo_TP&tPmnqf&ANS&n zponWKS<{fp*1uPz$EQ1WMoiBiJH8n1Fx;|Zo~`VsQ=E6uic_-BOsFw{Dc>FCgfwq3 zuEP^f3rx)K`F zbaAEy99sE&s+V8IV1l0IxleD&dWSfyprF$!?V*|smlZT8E*OWn#z?nqO!7maF*oVO&eiH{vgN@@sl*>8OcKBMMJ*# z%Q2~a1LkjUBq64n7ck)AR&1Ygg{OPc;>52p7Gc96((5+FlbgVI)D&65r z7E!$f*?w;lJh;g(x2)4AN*||yRraw)GQxi80pYyYUwmxUHckY%bualtW1~jWqN}fr zpe~;8=s1GN=N%k0dS<_;oN~({eC2<5g5XsUb_ z@aa%{(%+&!uL5Ic4XxoqK_7^oRUysWJNAIxv$%pgxaxEpBTI#?(4?sLcyMbQd^*FP zR8U8#9lt~2v7|+L@R5JHw*%|!uvo&dw#f%e#Dg=2sNs%oN4~8T;mFHuP4giIo?3yO zRU}IiFqci@M}DT>asm6Ph8z*)5Ve63Qla(O=&)w|w!LqHWthz$0zhLTdK}nu! zyl4DC)bM~0gZ|$u&EDe2NO%hNK+2X-Jn2>hb~_IlKCe#V6U~E=JLE+qfd}DE-5r=` zi)y8q)Ab9%N{>1-W4z8qEJ=5xmazDg*WD^8oR=giady$3Y`-N$f zNKCIcQhdGL&0u6X={22&i^*Zkdw;7}yW_O9o)1xZWcv*wtr$GQW_n%<$>-8uvGSw? zX^1^b{4!sg&dHt!>^QD4mt=Z84w7t%{I{+oQZf z_k-g=?lG{fd-G>~rD`rDM%wG-`wA2Jjexzu?v^4U@;7fAapxw@HgPhSoh6M?yVIc& za5F#muFT&9#K5<4SauyQ5qBt(4vX2Fv?w5$9ue!$>r~m1!|*}RU8>I_x57X2OeY^2 z$zXL4)c_)t-fgy96XuIfW<#4H18k;mj|OHF^^5yd@@{n&6O?2Mqs+xdUJ(4rhHIdn zp4p|QoY}6v7Jp#Msg#-0LCq)-GZ!R4cdEL3ZT#vu$#~03n=+{gTIr5YS8Px8t@3;Y z6Y<2KZgG7^dVkde1xXwxfvOf|MeLpEk716^>b8ka%XpqGgL|EM1cSL{HKFuT#bqb; zWV#R%MY|;!KO@-tqzL5Db$;hol0+3vyx|%Hby+SZJQ5BXil{^`a^T22dOGRtBo@48 zuDECjCk!}-H>Fc#Na5+-J5c|!9uz7*NL0mmWQoeH*zw@Zk*((z%7oK+-1(jeu@Xy?k0%1<*ftH9U6SMk*n(e& z){r>35B7hdC|(1NVhV+#3FbhaOP`P!Pmg(V`gzW=nsJS=&};SbqD;yN`C$LPRG3-E#)vrG`Y| zy3c3|F{#V`+=-AlukmKjg@nGIN^dHpo!Sn6>Pk1u{G2fnTd7IWcj_2J1PlNpVJhA{ z8@PQjkyY$?bJw~ME8p7E`xqj@qov+7NZX?ylR|L!_JJfCsPp#N-tM66t3m{a@d{Ox z+;&``Vg0TR8*$3+MP5zOBxh{u5aY0)$*p<83FMyBGy-Zf4hWa4z$d$z+ zlC6yr5N1!1!dRHy(#PS~!8vJCQb>|y3$5^u6{7AL`&bQ7$yOKyX6!I&Mket;P$@(I zamrp0yCHa>wW5lgR>Dy%|P3;A-PO)EFO;*zAS||V$Z7&$N3MKy~ zMgKKqNAj&kfu9DHYbgp#@)hV&B7q)$nq|(O10JwB$%YW0ELv<%)wg7TDSh?-*a;t7 z1LlqZAZ8H5eY&;KxZJ$MEX#I58tkyoin&Xh{P`)=JZvMmCgOl@M!U^qk=t~rTI2UVFia9S{b9vjz0-7Qso!U zlUt785w`fL^u#y1H~#&WUALXaGSR0p=Yzter_=!D?h?{|w$E-)1Rb=3?H8pcbCD@BlIb@~0}^P|;))TtIo? zBe^J|RcN^ty?dky0sxo7wbNoWGLW{JXU)fjtp0w0fWI_5(_8JIpBE8omT0N;LOQ(ZCROh;@l(0ap%tSw zRDsO4vcz40Xl|(IR6ilsRv?zuVDa9&N*3nbVUgabgaERA!$~;O%Yrg|dVmYNgalOO z9BRL`xI`Ga`Qd@kiud*gz<@@`X)qiRo%a0!>H^@+nw`X&PxpymV;^l*#v~YvS3QV@ zft46f=o$eOr~b9x%*^>)yuE6MD3i|WMJT;%QgGcR1%~dAi47xisgMm1yQLU5LafcJ zVP6o6*~nqs03lXzjA(S0Re{EvRX_2BY0CTi9~Q44$A@Xu8l=z0YSNDgBR4L;{^VQh z&z{2~I#nt7Guv;E=J~`w5y?xCTct}M{&SXjsnr4@r#m=8kpf`FK6zpR#r>GWGX-bd z0P(Es=9P>695Ayu8VLfN2Q6+PtwujDh!iYU5jtxJHf(ZvO2^7?IE{V2aCbR5jS^ZF z7vfENzExm1HF*^v+TK@9}n&qV6crtV}_`spoGMJyUs4G_oPNQx0|lV|eW4m^JW0Oaw^zx}z@@n-1Ju=CKR* z#lPQQjJ06)$6-e!G|f9BG;FZKm9w;)oX&sn?^t(|>MFdl>P;MLV@dQJIvqo@da|@D zvwPbu7frb@pBlNTDP<^jOALlF{*4_K_WnZ*R*qN(AnaYR5)1GN#*kR7ur8+-N8Jda z1lNp*JyTB7yxc43OdfrLIxXv0Lg2p_6O>l2H^DW{+o1erf=eCaZUMra2FJ)#lK-Mx z2w;N9X?T@pyLbk=%=lu4j?UjvS!LV<44xgC#D7;aT#DXdhIwQsE)f!GfcTUKzqBw5 zDxpRmTTiwNz%XM)E6mFa+VHL83?FuTDrTTn%3GwdQ&k%CRf@VXYyI?WmY z*9!$*YQ9a-S|nn&)aA|hoWp~7=e@*A?3<6Rk_T8!OwFO2tg6r@252iQ7A}U@54e2g zprx#X;xm8wQD(PP{HaGWlJTw>L--~Xqru|io?!F0GYdtI1L2U^@>y(WGz71WTt z>9Y`W8SLBWd)P+FKm!{OaiI!^OHY3cXQFKS6h>ymVWh&yo!VUmlunoL^|&=$jZ_rWlMj~qQ;(%wwoeK4rq!-_Evsznf*oCK3wB(^U4Qc~9NRm=qUW3p+!(SNLWjv{4GZmsPVjEquU zhgoEEQWvtTzxK;Ih*zHWCnoWgHL1!SrY@`r&`|u2>{|`S%$}TE0M+Hx>o^DSyUl(? z@%N@CNb=N*Vk@x`0eVSoSN09BvHFU;rpJ9=hPB3YkZ@MtK>|Sl9tV_ojb^a`li9FY zs?a*@FH#TZnLL(giievMRp7snJrLeSiwcKhnkxGvzZbauzDP5&&(s86?}u}8c19XR zow=z2u;fdwwj8wrgQXu6Nu;A%KP*gOt+YSEXA0hHGfH@bONBtPq3>WkiJu}GS&=Ee zP+{QH9q#w3d}87^PmQVr}c3+(k=*+Q!rnrUE4o09Y(H0DzA;j4+7rWV4uo~h8l-iX%A zdUoVnG83*{HWq$>$p?sQ*6C4GnUED6c67y35=*5NQr-Y5!xeMdhIPSIb5Dg(tF6W zdoXPDXu`Z(BEBY%3qd-W5Hj=Vq5JJw(cgG6{Wrqul!U|#f6Qb^Si`#ltVXHj4KNGf zqkfdk3k+cB0nc7|8*aNku=mi6m40*Q09iS_s60lD%Yvv@7>ir{(qjGH(C)u}MJRl- zMPFvn5z?%uHNl2nfGj;!W6*hX(+w;$K zmhy*&fP)$uLbIOT?r5{^l_=?MY3n82anD%O;F=_s08pMe%_W)L@7NANDvm8Hla1W| zyU=7jK6zszbwu4miHvN`bb?oYT%?7Fy z|0|ur{cPo5+;30FTiUWmNOxq-cqcdm<%_*EguqgC6$%-z)*~=U)omYf@T986Quwsr zqYNN|I%(Sm@R|3&ZU^N!b~^2QQX4ZA2BR~A0FQ(*+@UI{byb0C6haNM1~xe2wa@mu zWwPqe%n5JyKXX~C`okipE8gM}DH7#N^v3JIvJPhV1#QM8!DseL9$$B5K769HRO2bR z9;?}JhtOCm;9Duf`oJ|uAR8@=UB@%V(Dj|=6JLrhv~ywY>#!R@D`8eyK=f<(W3fa$ z%&`Qkf6%H&vve8415>+iZfHs47ah`}r-Z}+;uDv!8fX(Jf0NVBDK+Oselx(YngIMh zv$V)MsO|e(Cbh#sE0svSZGgeX7x5pTGr*%j-)E=D ztQ>&B;CE1dJ*UX@rubiVlb~^;F5YCiJZnS(g^;3We=u6m{Qx_0*i~Ke3nFq_748)E zvU+jim`<=p%{yp6m$!8X!LaPd@QoJKtic|Ly%E@z8}5B|SqDpBm(DyHlYX^Ef8jDF zBwT>+E`!=UEi^6nH$y|rrZi416=QJSdR1E`sEGiaVNADHq+>J6(}U#BoFT|gKl-hv zC}JYPT&I1SM(u**`7cLP18SF!OI6=59q9K9#>+QnnK^>&?OA(arOjKQLR&5oG48l^ zvwF}G<{F>pZ~&})<~8P1g;dWh2_gi}u|S!}g7W~h8m8BJ{CTo|&5D`A>33^;_4*Ug zHcJ{)=vvO=LWqMKe=MOXCHJ>G8tDt+{3RnpmuI`r_hhog2>8-3n>tcH^UBePn&~Uh zCL(26eo3hubrY9;1#FD_pf|U1M%e6_>8#NxQ_V#(aLfQ@A*lpdF*vhvnjT3pbj3)u z%MYHX%i6iD=SpC|5kSOoYmkg~`(PIE*~$QP`TO(Fn0oU|+0%7+GP};5gxKD*4X6X* zg<}a_mUiVXvFi91TLe|WMy&{3Lz~YcA@KQBf=ANI)fGYV0cx&2$O*kj@|_DkiLsl@ z1fp{Y9-T}zbt1FNBha4QMdu&nM>62GBDmW(&$?T7xa`X&Hu|Oj39|t^^K7E#5FIm1 zR0*|TasWeS?#D8yM}G|K{#>fWmic1qd6u~AFFoRL=thDWqbfm>=k`5>Pa}DXMNYBw z2lRJJyESuJRt-bf@No*f<}!tl97z5<@*e^8IE;|aUYeYm#$>Q3$=lAH1yj#REGl_x z7!04O)C(c4w5b2qJjX`YG)qbC#}|P$*eF_#-&HrM72=Fc*o-YwCct6b-HPQojI-AV zW`paW1KG;6gqK6znx0tfmd+Oln;j6@m!E5%8q?cG;x#U>M8vA1uu7|!?D)Y8);d(M zaz7SKRZrvNBDbH33L0KD$saF4)oM@T>n8OIaJ0`_@h4y8ff4#V;oA2=tBvziw~(WZP1)VG`4xMEV-rkXQJzc9#=)x= zjq4FY@n7CE*EO_k>FHiijG*7_5eWYu0Uuk>Ko1*Kp?PK9@~mJx&mY{YFlrsrZ_+^u zr0M@>H`iQ=go($SA+sj7{g%TK7CCbHA-q>N#@w>W-<3X(|H7+Cu>Yf8%u}&-M|f=v z4FPXJ^?iZ+!oL7_=gyjOsGph$jP`xd-t%a($i6=5w_Eiru`s(NTVE+~pcjIX#Td%Y zP0@o2&B;XS^XW(;JNzFBAjyvtdfE1ipJgV}Xy(h`&0PN@XfIh@JeUQCM0vhw_&h&d zgzb!0Jwz;qSY_O_&9?l*;SZW^m9L%lGm_j5vJd*@|0>TaJWe-_Q<-r7OsYKBEeGdL zW9s+&th8wcYHq_5t91dtrBNW*SGZQ3Z|}zZ+%fdia%$~`o1w3A41gNFLIAg8t)KU; zn78doY2w;VbL>ITmV<){6|vqlqv19~0va?c?#&(NVv#SGb^(5#FxgCQEvK&vWf)6# z0JslJJC5&v=hmXup3TS5L%o8N6E!`WxIGWY#FSx8ht2(ZI9$E3iBkkL_ z?wVIeq72ONZ*N72)l;M=xLA&e!7glgP`6)R+|W`diL8=$TVcI6$pV1M?3R#f?!kMD z!|3%jog;5r!+#JV$AMb2w5R#HIPrpQmSCp`gdlnf(2fEi#JN~(i@}khVoQ#PEo1d_ zAdN73kV2ekQCyP4#cxL+Q29@QlC0eor~Y%*Th9u!6=qfl35lJ-p8(7@pPc**QWudg z(uo1`wm|7*fE8X_Dq(Eh2(YoBUTG(f@z>DKU8|e6&V_}yG!7$D^l+i|#6Q{Hf)zGw zZ?0K9Ywv@-p-g7G!`g1&=%C(3_dCpnnzVP(R6#ED3*8+~?*tj#`R?#Sb<=JbVP9G@ z;<~c9)MVy%X~k1EZr6i(r+FAeS|4y+FHPTm>1&9_U14zWTh5&GVP4m@ z@d(wW;z!_SU#q$bcFKJ{PGZcGvhDaXxc9t>$1<}_*%y6n#=Sdny^Ub`@MiLOgHBET zaZ90dXp%wKOYQhVvwN2f<=B<-ZNbA>fYBFhuIaYFZs$6*BaQG5P2%+lN3MsiDjB=Y zK@fIB^VkrMj$Z6|+Bp(jKesXbtitQa7V=HzU%CI*kNgU5X}?0yY zBz5oMX5^mDWDV=Z0T%B{Gn4ULkfEBW{rPSc@)#G;(uuF3v6F8?N75y1XmLze1c;6| zpVoW^TW-`VI(}Z*TfoiV@^HPFn|Caj_9VUWM!>ftCI-{@S#Vl{{V&~xT$Vq=@Hwp& z;lMC1vkxI<)_g9LUrx^TxCqGZDbambOw9ycyouKPJiKuYtw+G{zm;4JVSZ_n-*cVt z=)`a6MZB!}Hj;~OmR^Ab&Nd2BR)$Aj@SF_0+I;^@k^D|8ide4>(HWO~n}w+Ma{DlWsHMshRyR!g)&OsOcOu1(5c0NvdpDk9m_6&0$C z8riipPAzMYLM%SvCnb_RALHK7;d3McnsguSugh?t$`dYhx7%O-+!MUsF4PJbQmPJ}&l-mb2E$0fi|JXm%6M`ai)y8X@5RP{h z24iem-YV44{rY``){jnnf!^}*?Btf2LHwim(|zy$L0F);rCNv2suSI%*9{@>`*R{N zdMnL%DCx28+3UotMkU)q^^g(W6oKPgay}dxzYBWU?dn7PhR5IDaQ|H=?{~a@{uDrz zlw*y?rMzGbF)xxVteDV|@It}mOolNAcj%$dUhn%A{`0ZO`)=D_8=zT{_pQCo(_74& zmZiryQWuJSbn5`WaVWd#JGN$JRfUf|G5LbRHYa5TETZq#BroVeSKSy=hB_0XuhzaU zuxT`A+y64G-p~{WF=A1FJVrIu=1x4NaV(cN};t#c!;I@ZI65Elx%j&yBO zNh^0w0?A4T=C{Vu2qtgO4mVjK-B) zbl7x5zW4*le!D}NCh)mn`DljMy!Fhcs{YqQSzmK<>P@l(-eBER5$0JZT`(^4US&rB zG3M|zrq|K4=Ef+*A8JC*x^gBw1=sb*JWQn0G?W&WKV(d?zgoKFhg#dGFfLZWGJ!Y^ zrV%yl6MI8YZundsZ7p@*UXxUulara(gA+JgPI$!5UEcXRIq_oj5W)WB_L_KK-5ZTsSe4m zN&pSNi0)04@)6bd(t9Q-ZH}?1WZd;;lK(96z1`X`o&NyYPtr&YinCJAT)bZ$HqzQ5 z@hZl8 zRQ!U0cum z`!$}4kvY1(tm#PllUv&mmvqIsdbnGWf^<5mxbh2S>G=X2YDJJ+yO}+y-ydA2(PW_x*+&Q21aBo@w#Cu99sCZ*J|Q)}#&q zamnuGi{EY(F@y7M>&`dv#<~0x1VzT^DcPC7V0fJ1vmrg1(?M-pEt(3e#xpcor z`w0}z23Tu&u9Ig8&hv$j7^)T}^HHpX-$qKID5j%N=?u~1-0Zt$C6h^n-A-~vNT2!G zE}%VCeT7E+d`;SnRj|20iC-iu9UDCIp-M~f-7vZAV@kfHGyg;Jklw_@A3wJ;wIlnv zGFhUO>@xi4RurC!ufz453vsJo)vtzr!BvaJu}bfUpu!4QpLWZcGN`#+burF7g6`IL z%d~A{#orO}O$ml2Z^g<=d}wQcbaa+0R~fh5^zh9zFs6~-&oz;iN z&uY4219{z}F^Fyq&c$P&ao3>Jwh`skTqee?ez4xKH?U~%@5$Mi3VcR4drEedqMg}s zcvT7WEX@JRve4x2^E>XrVa{-aIk$a8dO#dv=KVP2S$Y8r62o$tOiV`85+d@Bnx3S2 zbbh{Z#HjKS5_z|hM8wJv7G9YG)X9BEhuwx+A^N6->;*>t{me+tq%0f2HMk(ygvmGg zS-b)pQB!m=O(Zqhr@qy_%05SBhS%~Jd^}!P!;4GKHu@)g{&sJ2;BtkxnIhPBT2I5E z+J9B2adhqvto#f8y6TrW3B%7md?!X{)*Tbd;~|@=sbaVT>0!I@FqPfIKqSu}ep-Pl zyt~@|gX-2~J3rRM%i8|~Swh=$^(hTN-_Q5?|2mZ~YFR@zql-DiYtqO-b|1Jvy3K!c zr5tSHf~#|P%UmV2uV)gAR>cAq#%g#+nMXvZRIJ-sSfnK8;`il;L_eHq?Q)9X47B$_ zWtVcoN2EUdlER(|LbvG8$-et7rlga-KZeveI8lpd=xIm0{xPBi{YYT^jh=~5n+Lh& z!5s@}^Zlv1Mx7HWo%mO3aU%m|XP-~8viegNs=>-S+aptVZb^!cc5@px5bm_UO)$<; zEv<9ai^8Wsh&|2^0F-lMD^f%Bt1w;ZA#N7gm00ojQXRr+Rf#!nCF_iTXscWsPcSYY z0{4O->a07lhpkH4_h^J9^0v<^(2D5Sf*kvFKX9{Nw>f?y@NJ`E)J~M#ZGx2g|1;K& z9V-CqQemJ#Sh&qF$A7zG#O;c$4#*kwuuabH4oN;EsMk2L_+W>g5-;Ukl`MyYop*L= zbO?@mw>-UZkxuWV0))YOgBv#-uzWY)RGSmzp*+c=1i%hBa>CLvS`ph-xfj~ysWzwc zG|xA=02Zgee8Ed97!}96dF!5eFJpE*xj$E)o$;V@;mFCh|EfH zQ`3%T$DsJUQeaQz*oPlfzazLbs^prCYN1$!f+qHS=EZ>1a65?ce-M7w^{{0Xp2dEn zKX7Y%W6QiACH9pARPTbGh$pnCvL=am+yQf`YTHy1)9-m)G-BY{*b~|1cb9tExGkfp zh;}$7QT91OvTIP~$ae_!*DRo`Nv@L>UY6hjdT#9#&feFPsuNrbz3K2Tp{3982h~O} zP;KN5m3yA1ku*!MmfP7sG~0iZOqTVPn4j#1?XKC2sdGNshEyd2Y^_Rnz1sD30kbLZ z?#PqwI`#;>^#u?hhCxVy-2H)4IZ%Cg>la4*JqNcXuM&FLve-0WY8hMHD%yuyyhS{N zZ>P$iL=E!z@V#Iy%02IY2rWe@33lUM8y8xycmgH#B_2H#{xWcn$JTSGw#QVLF5F6m z9qT%Ul|s=#b}lHeYw)pHg(Dp;@iN9kvCEE0W9Rc1L)|4R_s$3Z;6HX}liE(rO;3lq z;L(GB958PXy|$zW1RswtY`QnAD1(gN80(*$d8bsVpF4-;$t5CZc!RwzGx$EbW^sV5 zO4MGPyb|+A9MEqr0LtZyOgC_cY?vVe=CcWOLu1ArsJGTK$fj{AQV#+Swlm;7dD33K z`!zBoIeoERSD@GC=Cn=35O!UE7*Xo`epf@@(aw2PzFEJKSgFhLUwZh_nUdsd2sk05@O?_V8`kL2 z-g3l8Z#LC&v|+XVYN2QH{e}~?`S|&39bHy4Mulf(p5vCwPzYv9O<@Br z9fKeH#DBOBUtG@!vQa_9@!*1n^Le5ZLzr?viK=OS@E)O7t|e5Tb{;8u>vv%4o7a7+_Uo z3GaQHV`uE*EHMbqDFwFCuoNonz_~o+*)Fd~Fq`EBitZOTf6F6rq#1WMGCJ^~=5kz+ zX6^rGFl^C~)jOw4KMcbxTc8-nD;0{nmgSNBsaiHfBo8;#Y^d6!&;Bt?wQ&RD2(iA{ zL8YPq@7Pe0qGiKN-ZKCkg+K0oIB4b#$E;kNZId{mqcYme z;#zgL{;70|oK<=w^fX8hUFT*`+tA9+c^tJ_)2`>*&GixuY7Rhcq*j1buH)|{>MiM4e zw=Ct93>3ec!4&lPqh5v|6WU9!lIs`n?CE@++y6fR`9KE0qZ{GZ?JZ!u5XQdaP_^ee z9STi`)>2>0qwDxK^@E6vhMqw?qrV*yD<`MZs_D(3ujC$dyXhByJhkoDMp1XaZ7^<# z9hac0WhI>|&WFzGehkue(oaG}%H4f_Dk~{Y)gYQ5QsRL$>6r6e3yBjU(jsG67JqeE4VptbGwbQ#Z@B8UjOdh`ELnXj@G zG12L5T6Xjb^pyWvJ@sOIUU0Gdu#d-N;-B_jHW)`lZZoKwIGj#x8Uw90`Ihby%*k{mM%M!Xv zVYhsU$m-WVNM)gn$oPD z>T-saA?ixMr&UIA0QzaP!lbJRVSI1}s>T)2q1+A7T531C$+s7hAfjO2yPs2;?{CWx zb*pk{S+`VZC-7uii0&f9Hy=UOfGu>WW}_E6`%_oCi2se_5K*z}ZK?~s*nnu8HL8*d|6#h1%4*JCMRYA*Ps?`fhh`qNuAu7(kvjmY z-q}fqjNX5N)`q6hHRkkzh?=dl-=eyThe?R4x;3=y$XICRY^9&BBZO}pRIMCg(4jlJ z*Ej5>OZW;VLqyTo5g{teyS^Gx71%(_YKKBEjb;bwLPBhSs-zF-ROKaT?f3_D3A6iF zh-g|p%A~Sj`;QS-*GyWKI}TcTG`WOsB*dvCDDpaA@7Zf8wAL|&E^(_rL{!xkS5Tdi zvINmIbQ-N%H$DKJT*{^^332l_6nSTxkzE9*f!Th#f~Q3iM0D*+@l)Nm2eJ@F4LfMn z`EF&<$nCYC)0KpH`6^W1E2KlyCPQxZMyCI_Oop0c@WXJxOu%nWuc7D(R>f3RiW&8(8leX9=enegOj1^i*!0QC<$77 z^O(WdEBFc`7Ls2vsjhxaDx&6qpH_{$1Z}iBM7I*+$b9DuD%5?~2wHn*Bwb+mVu+X+ zI+8X_xc>;zvfzDMb*vP+m_Nctw-O@#O{n_#5FIkAlA*PU*>q0ZIU|udzLqv@`WVro zu<5B_95iwJULjpeh}zXqwLgbW^;`w56}=K2 zYvpt=As$bLsxfJF>S6=5mNffw##r$1S%{d~GdDs#G zzCowXPlDFG!)h2~JH|r9(CPJgROg#B7EzJ;hDnQzsg0n6)yE9Fm=ON9Wl%KnJ31BG z4!sSiXN(QZgovfH*|g#FwLc&lylq3Y>gaxGVE-F_x|tCEPx7HC^&A~*7J$|oDi~9~ zO)Vi}%Ggj4i_(!BdQPM^_cBT$moBub))YlgR5V19J zy@xjJXo6@sSxc)14~6?ZTTYh~Vp9hwDjQG74lah)l1DI>jD43NVy&T)c4YKL6bvY# zRq0>B^^!mF(cOeF4v&SZ$#g7ZE3~%jN5)dktN=u;1zJz@QC)6vJ|a({W5qWaZkKdn zI^9l)v^SyZD?goTzYALHIEb-y>k>rloo*YV4WsTN^7kO^sw=ft(QiJ4{x85MtnHirU;!d}xW!+NYZeubGx}FdP+o7s|Ih`ta4ti^C zGInnDhZ~5%lW%Fmk?#B0{$Hq7D9bOAulC zU1{0e<8ZUiDJF%`!E&g&a+FR@ZwvXyITH6m@=qPTe@=h1Qn#HW(una_7-TJ^Xo-0SqaAqt@d8a8nJe<)Qcn-35Zbst6sk7VJFjgY3%%VhU@QcWO@h0KF+EJ$ zaAYqcDPaBeB(HZJTx#a06ha&651^_`37s1GCiGTX&R8(3CcQflRBY0! zePiHGEjLjJeVDDFYDPJo+A;uod#V>5h^Ikav3PkNZSb7QLuC4 z57CbHyAd&esrJ(%a}8Xm=@37K&`P^T>ffnJr`A6Wtpyg;FzSM*;7VfRAd@x}uE|8C zcvqOTD0uKaxX#!SK?f4Jpok% z>*>^%{?OZ%O^mjjesC+{ojIL$RPRTG^e>=QgNMRhZm+4K5ZbAj1XUZR)2VN_L2sR_ z7;T?)f@_J+89v%jQ(T6~*gt_*r4N9+3@E1%`kB5Rs>W5&sn8eD+tuNWGV@6Q?j>dn zp$+Edeu#{D`)E~gJ6z>>wVy)h=l`ki3SL%1$2L5I*6L;%jIQhb;bP+IdOvOOEYCtj zY^|bYT?25Dq>cZNLTISwOaO`+7tpZ-NzmIvKcmVwbqw503_V6W{`M>);`Ulv)-4q- zGHGxgh0u^^Z2*dPAEIM}ZbNTfzGhT;+Q8LB-SW3+L+Is3h=in5wCvrjaF63v6hcd8 zMi~@!I7Y`(o`&8AOpxjZ@e?OD(~g1}h=}KXwCd;>xJKrwdJ3T>-<%>SnwL$-dY^{g z-ki&5GDn<-yNRR&CT+<3U>71`fQMGiO@?cfj-n8ny0Z(48Xcu$o0mXuW7{*D3YUA~ za-!uXgEpjZKqT~^K&$?~3U1N&lROHcsmkY|=!X?=lrgfg^R%G4g2vEW-3mXWC~ZGnPbA-=9rX*m2)`>w4O;ZL zDO}?9Gzy`u>)W8HXcirtHUOGCXEKV`PlW4ztbydRsDD`Tw&e|ehQ(j z%T1vuznG5I&V=SJ)H8bgtsyjUyxO1*g^{RFNSflKMTO1F;0A$awG={M6(^x;7#+(k zhUR8YWAx-SLBja|?A-@nQ|Z<=@CB#{2v*1}2*oZ4Y>f!^BseyLfXW#G3v3%8LpfqW zM-izasL>I{MvN2{MX^Oi5wW1eK95JA(KFxWedj!VMx4E~Q}!L!{{tv}{r0ujeceo3 zkdGGN!={4ud1+A~zDNaf5PegNgfPhRT7bu9lu9!}32aLXNBfx)1uP(US7^}!5i^Zo zbx$%-s3|TW14k!DBEtyLy#leex1&`2K_#%QJ09(ZJLL^bAji|u0_Gz=V0A;oQK;N( za6Ds~0hva~`9Kh9{uz{tpQHk|O&`;95pu8- zL>lOek}==y(^F|iduiGZj37hP(Gn#Cz}kKaL!r7>g2RQBMk3n?895C^8dHfXeZiAH(&%wSc{g>+(-c{b6u>nc(LF9j2Hijj3Z>qF%W6$UX(2B0)H-%OIfHvJK1+q34|3P zo@j~RE*ZhPl;;&F5Pt<6EO-%*gs|?}HW116BZpF{lYnvK^k^sfkAbiwMvazue$R0Q zBmDWF!Le%3nUN6IeX0SGyo*gJRYVyuZsl)iCuar#VMppjw8Z^|DPUd44JeeSD>&5j zIUEwgJo7pb%L66TT>{4W#G#$cNdUr-?Pt&uVVl9aR>q-FXY0X{ex1c5A?!16kkDCX zIgej}ZO3k+oh(WP!jRVG99lrX%m`MMcm@UHDyM@3Dc#Q?A03RgGSPLOoj#4z&3>hlDtTJ+Y2xd|#UYY-|0%lsGgQC2KqYZ1dj1ptd(0q#};SdsAy3g!_64if93Kti0v%>WQ7@dQei zzY-YtP>uGWI~5CrF)fXeXo1_i!J3A0DAe*!aFE@}JQCt0=GB8pcb{`8**aicuP@pI zUk-#hWf^Dz-qRbbXiyvq)tC*Aap0^L32_#2(?O)vOf5=w5g6xFiT02`3}hKwz6&BicjqLm=!~7=acr?+66z*}NEq zQXd0{sA)n%oQJ_3L^4j~QL?8&z_u-)&1eUcqk*vJK!X;=Gv{`L^^Bg4LglXqN0>%A zFJYSW0(#{v1xofY6&QEQjCN4683=n+GYilHior&(ntwZGLV-Rcfde#qy+cBr$UZM6 zh@&jmqhx18fN}PD1Qf|FzJOFEx_+kfwlNF z2wx^n06QD~nMWr>NZ0`oDX{}3%dZ8-4LM<#jcX+kCRJCUB`U9jwTym^LLI&b_7ynZ zfX;@H!`&d#wEZaAyTQP?ph+gQIl~(ujH+!z3+yXXf|a;5@+eUJAh55cRp@jGnY$81 zN@zgIPVWK6HE&X&&E>}cVYXfnEitJOti)K#qd*<*U{|i5CUiQ4gnthr?V5s;nHDL5 zZK{*)XmhutfiOxrT!9wIPXjAaUeTjK89882Dev>q`4Eym14OF!N9k_;3~W z)D47HbvMxh{Bjqtk|!A`kogMOQ(Xxl`W#yHG?PJ|G1Tl`|r9XTa2)F@ywH2z&hOa zqfnFT!EUaXn9vCj!tshn`N^4!hP%&C3^9Twlw~O0vz5TO`cSmFbDM#%YgHRsAbluU zNA?^Ps?rDSC1gY-62dy}etK6)Pe!5z z;>Unhcwa-I{L;Wq?o}coY~p#%yZpH;uH~#-dLS6AW6&p*%)Ag7_p=v=w)QCs2*cJd zF{60;Wk#?D)o>IlG70P>c+(F^2z$8G9g}lMdTiKMf3(L4*0XdjN~YZdtQ&?l7dHS1 z!-AIcC|=~=RsTQIoUS9@lxO@bKI+P7|@c0bM9wB$~=6qhWY~A#b6eHN$QBRcac_J`w zYNi%#%x@47rq#ux1u|;E8r**5P@pU~u)PP1BT=>p;eKe#4}V_jmDGDVTLt#9WfO;z z`CbLah1}xM#`4zzVOq6CT?OXSPOyT`QWQ!Z1vcmHp+|WlM3LXrFlE#Dn8dMKl0h$Crv zy&-PNtqD)kuGVh>xkzh5=|*e;#toQ@HpWj<0o#c3;%O95yTT2mdr*l2nWr>^Ej4@X zLqZ(JtO;K-=kuO%1KnJ`l^`e4z9?NqH85^h7KgUQz3v3Yk$WX*f#mHVT~8(o#8no8 zE!8w3Ar5jT*Z(4hcj=cPX%rB%MJEOeqA(lshyi zQ2a`;oxmFzS|r4N_>8=U%;h(R-Ye{E23e~PMd`vX1LFc;-9Z~meh921EulP$mp&gP zQ!a}@fjZp5cEt07qFQGe;pEHW1LwLk)Awi(dwbmiMvvO6~?tZO6K<%Soe4z+L+k`m`CP! zpm^rc5Rfc74h1rmgRT6$@EsCjOWY5B@nL6+-@iR}G*|^pF&6VESw&J$L<{VH3zAI@N1+U6^tu-D1 z){&IKCKQh^2gwRcP^gI^U>k)KBaslRi{v$ixcJ6dIlBiQ=u`r$lndKXGQJpCx3v;& z&9@Fn|Gec?TX7epa;-ptI9pxcuX=AlLaa=oUh;gT$Aa~4uHHsq+l&m9jQ^|z#syTM zt?7pX^GNGQv_ReP2XC zHv)lqp>(6%fN?Gx zIkdHjoxnVDwE-VnFfQ-YY>0ql-&g2MV>!Xp46tI|ZZm<%VM-rP*JpPt9SSYA4>OxGJ$Jz!x3gr<5)}~x_8!bSHp(*UpqVX>h zcclh`lRG*JrAuE0tc%V}{e`sS<9B?nRDXBpj?QdGfw<*E zz}kGmv?vZC`lif}8=s_I4pu_H5K@8C@ymd9iLcStxUd7jJfeJ}Me*{VfN-jyknW@# zr$%O$G#T!rc#TKF$~ub`C=?+Y!@0c0r^dgiJLal_{v&lcN~b*utV{S&hqgBFArSTr ze1_tg7I}jZ-lG!-4jo<^mKWEiKucxa1?&3d_!$&}kUQyO2Y(^x!Fu-83IsX74?0esa;zc}$J}6y!s-xbPt?>`7QY$#L0>5am zs^)9=u`h)1ye7Y6@}wP4-d?Xyg?`NV9 z$eIqUiwQ^DyX6Y(BV`#Vp5Gw&oiTPxNL0;}32Uc>%}rOJO=w;j!FqZ;PUHU|M0=+# zJv?jep5yb!OpAs8L2CR^y6|XVU3U}OUPm{ukBrGf@%C-ZuD@6JB4>wxN#2?K(>k;r z{$mPQ&%<;0p9taF@=okMctiiWv#v5ew$ME_zkAdwQ!9$X9hYpcNzm>$V)>(dKlSqk(xu zwOWT3;Lu)%kJbsMeVR1YvNWq3QjT|H231oAD)=G4~d?l+b_EG3alH~j&=~a zIuMvgj;EuXWqbkaNbBOTA0eC~Zb_*Bgp0jyok1x;zA4zvXV8X?{x`#Kw<$QZp4YCx zx|RFT9_S(&XW&z!MHw@fgH^QL;;>&Jk-T|lMpni7;cwR0H^W~bL0fyrtuFpC%Yew4 z(8?w!0PC)+(H`Et2j-Dje=W)wKkH|(g65eK==2C@(x-=4Rd_5IbkA)IkZ%g?3EBGg z%dCnyzPA-xw2gUDz`D&1XcwVwz&tYk9m?6n5U_&YCUkN{5ni%)VtMU_gy3c+kZ*BG zNL>2(M(OK3b%sKxMcX+O4Xkr(M7!{N0?Z?IYLv4T^&s77e=QOx#q$N<#f5#I@c!Y2 z(MBNOrW`%+`@s`DpP$)@b`ZA?Sl2oc?Lyah09Z$Mg`%7txd)P^EHof-LYyMvQ(jon z$)s(Gh0Q>|#Te9icj>AajK8+9=p9rJMEqQ=}Vg9#mlFH!>QhN{s{O1gn@#PuM` z*cpb6PUBF&2L+U7Q%x-o80)V~4wHkX$5Z-$@0RdU;ma=`a#qB_W-d}U1o393sK$P!{wDDLMs+*&i(eOrqQkZ&^X zy1p*QqbM}*CCXD%3@~pIk9Kq7DzJ@|eL(qA9|H+yoYNv32xsEk&gI?o-?M64Vt_Z0 zZ!iW`CtP@Q(sOY`UWA!L**fNy#mWOu4XZ{2%Baio?Y-wH@ z!9rsy%}8V_qwD#t;$^!NhXkpBd_!}!LhLNjP zC|kONDPWn-YxhxhXwyh>TA!C$>b0Tf@~=R?MX8#$)P3PK|6H{eS>-<(Sl3yiMSHsa zUIpwTqy15~%!gdSGD#gsDc&zqcCJUDH7eTWK3-%@&7Th=e|@Sn4!<21)1!=`Kn z*7Z!{(XN`(fMH}0hjOJoQV16LNk*jbd6fNZdvi6uDHLw z)M}A+CHH`Jv730bD|6-;U=U$oU8i9TSi<{hJ31UOneLp<@Lh3!Vbc7q-tY}XY3#fg zQ?_wpX1qp=&SHNZurBcA9kj1IBSr(W$ki;At+~BmiO0#2NF0jO$K_?Mn6RKX@ow-h zK>DPNtsdR&eraN8N!|xei_S#95?H6a*p7DAHlZ3=MS>f7l&cPRu)yXi91_P-s7o3@ zmXBMS?d_rj(zl+L?(GX+`QKFMw-ulh8Vbyd*@yO)acK)MiYTAnN4d%<1M$2)ULtV- zPN6vMS2BCTmxlx0RPYVN*!-aPxYwp18*XbkbXrf8z`DABpuKVDmc;_2NK!t^mF^^n zH)tLbdoxF!AzXR!;Qg!sg< zIsn(SDiByiVmG2}t*8gl2B^`-h*n_;pFFB``0nWsS_6Uf$=I{2W$CItv*%QWKhk19 zlUWU{3qFsIz;&%k0Tz*@c9g3lV?eZNU1&?>Z_h$!U&~303T{pT(kEs0v86A*T$}Xl zc7{fw!+xl07qITxKkoI7!2T@n0Wj~24jsbWwICMQL+U4?T;*>E(LzR|l@aaB^t>g9u9b});5H-$ zNS|V>1MWQ>UYc35KYu5O{anWdU|y0RI)=7mbu}=D#GdC+rXrI-v@M&^ipc%+tV0uD z&6t1JJJ1NEPqA*@H5>MLo=_Xw6g(2dPcgu}w_WHc`5vQzIb^&BLyReXO3WhyHGL|dPS;*k3p;gc&TUV7a9U{^Dcz9_wSxm|cu?m4Gw z-a8$K-$uBKpMiNhcyy%5d4ofMC1jNWWlB92M7v&sf)TE*X^#J_@h`>x>e?Jn1AXAP4^TubOLz&fVt@e_upHQZ5uLsehGC3rF#l$Q0@4`b%FO3^J z#`PBv9zyxKT)u#AaQfgPkU3}q_c9YhQ8=kOZ{$1D8u zsxnV4iy7lq9SEc^F4fZp>>j^qN<)*MfjH z4H>~9>5JCjo9Q{>NzH+8l`#;Pfax{I%O_TL7}|E~v`8!wUjxh=&?JY4{FoGA1W6fh zLV41>Qi52ErU2|42q%xhg@M#uBr z;0=r*4;3g+=7UBM&C7)3f8$P{>uQ*^V_0ugKxzt*KBNREY@NSg!mY5$YQ2s_sYu%} zU|w)0k1`N`DFoO+qT^AXw1=8Ow6X~NFGLa7aJIDk`1;Y!eU~v1WynwSA1^CD)RiBh zDd0G?2Ewlf<^@eOp&X=N3jijNpBAG$anB1uv?MhWUZH8r&+?y8_F`a5ERen!RjHSE z=j`xYJU2p%Rz-9hjlen=4+YABw!P@G5*R>Qi%_1phA|-8_Acz-%xxW$b7z;=4t$X9 z4WutYtu5W|mnOch$}`+S>m%H$U|^l`eF4gYY40Ew#wq#x1RcuL+(97Pg~J^7TRX!m zZxs)J-0k{}G6oV2yH<-+7UE)a<9txLAc z9OWN@L;=jQ4Hqz{4BcN5>fFFw@J6{pZ4v4h+J zz&xM*C>z?mFWx}p?s}Gu=P0*tURYz%wbkQRZmzkF!s)y~w3HP*_7|pz_{D3V=ENih z_g%(7j0rwt$6YEN75?Fof=6O6@fU!3$0|`iIQ8lwKx8gt6o=NQMU!IRJ44=U&k2K* zYPuf`f!|FHM-i^6KrCY^-yhlmA7vzGE?Vv0n;2pQ(q|XLuB-0D&-)+AQy_=Lz6|q$ zdDALTPW)aa0Fk$!J$bY)UXz};By3Xg>NnBH?zVax;lGW}MG-!RfLO|n8gyp5m+4In zhtB8JYz`Qr0`hgnEeTt^G zwHyk!JQc(WUc_J-Lz|)F`SMG(E5~Hh9PLX=bM^Fxv3fI^QV#RJmc4)PV{QWmG7mbe@5YQP2*m@t#$<>OKBfa1Vb%I;H5?34vz-0QkEejxOdN{O!O#wyV;vo zcKg(`HTwIUr?(F0x3&MkAw$URH^9D2Z755edXFm*IokFNMaWnH5)8{n;X00iXtgMJ zCPhU4{=LQHzh58KNprLh#=xEfV~4$(xc9JM0f+1%8SjC8GyG7VxcsNtKxAmZYZM{k zy_6BKI}e46TMweew4;o*f0`RMY0Z}hz`mF> zC|B>!PXi)9cW`0yPAixl`-Nj5ZMVH!J%;b%9TN z0wOmH3s5*kv6N|=QffxQ_~k|rYuZSZ2VR{!^89f3f!S2X(5KGCK}o}l8{;GN3I&hE zsf>C6%>HZzvcyCjrDtSz|_r z;WP4Xj`}!r=$L@&W*}b`lrZ&zyH`=z-0;8sfW&!C9^?!?aXV4g3T_PmA}ep3P&mFq z(lo8{L&3O)F(BGWGde{3{!yM2a(3VQmC6|U5?f#Qea?j0CAak&1&74Bg?|t1JKm1+ zrtv=pL{7H(qHz2nNz*j(918cW7evcxLwn-PiU_~(Q6p}+&rjTy0_1CqE3K#AUqSdO=UUUk-_!EuA@83pc zrv?J~nn2h3dui{Br}*Zjx0|s4Ag1xazN=?Y?%F-40+A25N)#c}1ti$D0)@*c1JTl& z&@Li;50ze7P%|}_${6}!JnECS;pFnrw(CHs?m&saf`Qr1TJ1E!Zj6$H8l)v#iUXD9;ukHV17$$PYRH)PzFy+oWJUo ze@TXW#VQVk`xpXZRgXej;QdM}if`0L zy9FwNeASky-lT=+S1kEkyTZh4v7aL=>VbLQYmfzxidulkgK?Gug&P$DQp9{f!MGn= zKrGcH4y}mi_4%I;`@dRH)9n@n*FP`Eh>_Pb5>SsjR_^mvKZ&~fDRj%7o;cTpKbA5z=~J}i6X zS$Xbsfx?V`KuCHouH`XQBmktCb_<0w)PPvZ@ogvpuW0iN&zjAbW@lkgxc;y8d2G*|oDv^3pYC{02h4y9n%?p35O4GA6VFalG&I zQ82Eu1ti&MM&aHYr8fZ!qVLK)xUnp4K(G?X*C<2AY+vOyVsBI1%ljOMg#3m4Wx&3c zDIBsQ?qo0!hl|Na;ldt(B<~|oxL2_t*7S2If?1OhmzTL|{JN!4At~@R)WzT(;`9B& zO%+GFA~@_DAv#`@;dd_l=GhT@a@@y00QPy0M0Uh2>;d9vWBpLLu&p4;<8TzNG!?{Z zNk)MR(w`lvm{m6RLF*P3kUvxQ1SCd}yRx=qzdC|8jlO~0j|iW9V$`J4S6?1SZ%uFw zjx~aFDCLkHXP(3Yaj^NfQMj-JAj$e?C|qMJh*g-20%TN8T6A*N$^*?xAYT)EV7>eJ zYqzre+Nor>Z$y_d_sEaSpL&%&?j2L8QYyj8?9m`Yy56J!ajd%AC|vFXkYs8J3YS#~ zVzsWozL|JUzIyVCr%%$l>r;VzMM`YygRxI0d|q4?r$9pfgwrWbfABjOe)!PxjqlyZ z)(vP02?S^L#efXaJ=+b$v98CVaJls$$(B$a1q(j_Vx^X1A2jO2qh@bhwRBo_kP*nA zx$K&Hb=|^6p>xCYH75K6Lb#m~9sgc3>g>T+@7>o=O$cE<&=p|lCuE7KVI>fUO8kVv zN!{sHt>sa$xM&b7$OHe3k2^8)+CulbX(>RyCZ(`@_mho9uft#N{Ek+3$)EhAsdCcC zHC|=zD}4g`Q!`lfJYXh*^PhKipHM;)>Tut^A15qJ8e4a{zSRZ#od;dW7VV)#AP!XD zA?Cd4U64eX(}sdYB2tw3Qm;lB`KezZUA?8G%*U);xDcMDPi30|Q^#=M(Q z4a8yY`l4`ij)FAZ=_r_axe>&QZY;g=c-v1$RY3kM&^sjY#rRD>K7VFVAR(NNfBAc$ zJ2$gv!uU61>&DdYiiQ6|f;Mr;8o$-eKpbV-O%#k9RSnYAccEaBjV>UTOURZ$Ab%D# zIbWJO8jE&}f3tGHR|8!Vc=8Uih6`V%1mYmm4x?b);$V;_ zArpmrT?o<{;>_SU^c{ zW-Lh4c@Tvw83PiKl%OGQy$fEg&CF64V82B;4s`|Z{M7UQeX-|`h2yp_9rNI5(BFal zr5Ft|DD)~2$8d{B!L)l-AWiH>6z*9s5X+>rZoRsDS@D#)9r>p@E%sA{L(M&%wocnmhM- zE%sC7ba=@j&+{j9lD1!cu;uSSEO5^kS#)|<2oOhT=|I8w3dz%S=xG!z;|UPql+D3M zyWPD?8^iXuaUAwPgj2lpQzy@Tz4qGbRd4I=J;?U{1&HOgE=Cq{`7^zNI6%D`1>@IB zo~DDuJPLaO5#*O(H=o`Gulx^($2}^*eui)yuZinwc-^@6+VF8T_bAY{mcrS{ro7`R zK! zc>J8%*KVxZUU%<60v&X*4P!Ynik~t7WR31ORqQZ=M4cZ|xD_oxzNWb_ap~h5rAPLw zGj?jRpCBBkNN>9Nd!Tz`)mxv@)xkk3CD;(jL00Wt9stDNR{Eh}nmtO8Y7K{iJ--Zp zKvV$(hc272_K5mE@;dAfsQoVz?|k>q!&BB4Um0ARHgIZ)5r}=%`XQ?f`YYz9s0e_F>f!Hm+p;k!^lV^ zPK)Cr`GW7hi(8U8yZFlB<1uwnt|>qqY-=d8OGkHy+nF*a0tGW9fn;N{P`LNTe%!^7 zdi37V)gv0ix-v{ioEFFbE70u(!$=D7aS@tXqi2bA#@hDi_K#(k@_&y4D*0n!%F+3Ps_OxP-@45R(oR3MdFaP&I zx8ju7#obGNrj8z>0wQ+{Bamery*jlg)ma_|Q~v~#DTnJ(u)~GW52&(#+BS6IqR{=H z3<^3!>U6mK8F`1FADUf!a@Y&T0$oy@j!fHGJOqfn3<*WylHEYMHyJ3{nW^C1j7P7o z%bDQ$IvI&GG4q_ZK%p@F`zZfS8y9YP5j`Lw5ZFUjb|BmQ%2I&XNlPvYcQ^o~TknU0 zeToJrWen;J7+70gl-ZE1*CKHmyh5L^p4U~fVpI8(-P?So26U=`StKzJ8MkC55Iebd z6NMX<4bp9$hl2SH1?ThkArG#;eX^s%x5H1*BXJHKM*btY>c}DgP32D>F0HTL5)^0z z){$&qEwYV2I~9n1^gcu3Dq2ChX_Y8gMh-X?WzSDFGs;Ia`o2Tr3{0jU6g%5L#6Q0^ z;??*K>(*Z$-3-Ku49PSjn>dd9;ZfYYt`oCsltAp_aX1QB)CtnL&PKsB83xL^=|F# zaRFil%PR^B+8;F?sTlEU*@kuV$4m=?{(z(mzJmSpLq zi2EIsHI8dPb9hum>6Kw`@3n4;Re{5hT}2!UcM+^%yBY;68N=QnZH-#GZhZ0HrgMgN zouO=>T+J_7xI=6a0CxhqON7S4Fs>vPu|`ZvT>f^jhNw&wEPN=iKp9&-`nvn2kC`QT5jtcI;d#yP zfo|5`<*#xcub+PGZb&Tre}rs4g7TqApImviXvg580|Tg_Qhvt%6NPJ957uyWEr&v7 zl!4rBsqfx?(PQLcwV%EKnL>2P>(BUB?)4wBG3W98s6Pg}gxsx=!D%doM(=mHv280%xKKi+=*EIH^OS%sW&dLxHK; z$I=F$Z_Is`t}r7bh?YD3;aqZj)s%@I@7?G7MBQ}_f^Q+j#e+jfFh%S?v3GXyjoRL= zL%dy#AbTWr1PaI9x(ilu(GP{Z zCc5?n3HT?zHcFbf%>7;EvS`hOWF`KST+Hh%+Ms?SMmG?-$;6^X#eBN$W2^ zIO?qg#*mgK6yaJ5Sj7cj6zur`S#&Y9wnVRbVQD_D-pwh%CK7!ch2zS>I;MqjDAcn$a42K-v86GC zXPxc(WYF+D${SI{%?+F4U+S^!&B_70dSYEtfDlR6SJ|rZSy8aBtt0GK@udapqBn>9 z0&F7}KcH|CIba>JQ*R<4gI-2?yoP%+e0)i3n6+Cjeux;;j)E<&;oacDgXzsW~_EPDF>`?_g8u_`4Hr_m$o z2c@e9tixE|j)LVF!JfR`uGehX<5{B4Z&UCnLxe+aeiW$uISjdlUzd7TF7+=sv2}SU zLgh5Dk{SaFRvHYprb^A8I=1ZEq}<8pG&+ga&^?yDDtCi_Kb zOdN;uK{)&&zw5-5k;`{1Jigs$GzGd2K^FLlj8HWCjHXeZJC@aMzuIs57fADY6AEXV zm;zSPl7)h0C4fyBRju6*mt9-?I!}+XLbsx6^pDbgGd-_e8uqy6!qiqr&QL;P)F_Nj zb9>(0lC#CD-$W1Lyd>v2>kS`zx|*^~{_-<|)JFVIuKW z#F5D&-TCL*Is^!=j@x=^y}1{QhN|1U z(O4;B=xImE`au3<+=bFdxQk;%EE#$BZ6{`dSrSS8WXCu6lxmwL?sXmm$^EJ5>S?$_ zwMWv{xm97ez;4oZbh5)0JeU9PUe7lv=uQ{o;MLg-CZ_)K1~L;3HE#AEm+>2 z@p|tEU^zA>R>e3F=QJ+r^676{{j-c`TU0t2bQ(ga;9tjzd}JuZM-0WLM=FpKDIec5 zc>Zm88ZFK3#o?FrN5cEco&3uofℜc1ol|sFBrmANv&Ea$g~7z=c{%%gvB6JVA~D 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 literal 0 HcmV?d00001 diff --git a/frontend/public/images/integrations/Splunk.png b/frontend/public/images/integrations/Splunk.png new file mode 100644 index 0000000000000000000000000000000000000000..54f13cd251c0472189a9d43e9067f778c7000012 GIT binary patch literal 46489 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%0DL6Xx<;|3uGX`iJ#AugunMs)6GSH=+)H*FDwk& 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 Nb5(5>!ZVxj{{x%>oLB$= literal 0 HcmV?d00001 diff --git a/frontend/src/helpers/auditLogStreams.ts b/frontend/src/helpers/auditLogStreams.ts new file mode 100644 index 000000000..c3d36a512 --- /dev/null +++ b/frontend/src/helpers/auditLogStreams.ts @@ -0,0 +1,29 @@ +import { faCode, IconDefinition } from "@fortawesome/free-solid-svg-icons"; + +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { TAuditLogStream } from "@app/hooks/api/types"; +import { DiscriminativePick } from "@app/types"; + +export const AUDIT_LOG_STREAM_PROVIDER_MAP: Record< + LogProvider, + { name: string; image?: string; icon?: IconDefinition; size?: number } +> = { + [LogProvider.Custom]: { name: "Custom", icon: faCode }, + [LogProvider.Datadog]: { name: "Datadog", image: "Datadog.png" }, + [LogProvider.Splunk]: { name: "Splunk", image: "Splunk.png", size: 65 } +}; + +export function getProviderUrl( + logStream: DiscriminativePick +) { + switch (logStream.provider) { + case LogProvider.Custom: + return logStream.credentials.url; + case LogProvider.Datadog: + return logStream.credentials.url; + case LogProvider.Splunk: + return `https://${logStream.credentials.hostname}:8088/services/collector/event`; + default: + return "Not found"; + } +} diff --git a/frontend/src/hooks/api/auditLogStreams/enums.ts b/frontend/src/hooks/api/auditLogStreams/enums.ts new file mode 100644 index 000000000..9c03dc2bd --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/enums.ts @@ -0,0 +1,5 @@ +export enum LogProvider { + Datadog = "datadog", + Splunk = "splunk", + Custom = "custom" +} diff --git a/frontend/src/hooks/api/auditLogStreams/index.tsx b/frontend/src/hooks/api/auditLogStreams/index.tsx index 72b1fba1a..0c2adeab0 100644 --- a/frontend/src/hooks/api/auditLogStreams/index.tsx +++ b/frontend/src/hooks/api/auditLogStreams/index.tsx @@ -1,6 +1,2 @@ -export { - useCreateAuditLogStream, - useDeleteAuditLogStream, - useUpdateAuditLogStream -} from "./mutations"; -export { useGetAuditLogStreamDetails, useGetAuditLogStreams } from "./queries"; +export * from "./mutations"; +export * from "./queries"; diff --git a/frontend/src/hooks/api/auditLogStreams/mutations.tsx b/frontend/src/hooks/api/auditLogStreams/mutations.tsx index 1ed93f95b..67246baa0 100644 --- a/frontend/src/hooks/api/auditLogStreams/mutations.tsx +++ b/frontend/src/hooks/api/auditLogStreams/mutations.tsx @@ -12,50 +12,54 @@ import { export const useCreateAuditLogStream = () => { const queryClient = useQueryClient(); - - return useMutation<{ auditLogStream: TAuditLogStream }, object, TCreateAuditLogStreamDTO>({ - mutationFn: async (dto) => { + return useMutation({ + mutationFn: async ({ provider, ...params }: TCreateAuditLogStreamDTO) => { const { data } = await apiRequest.post<{ auditLogStream: TAuditLogStream }>( - "/api/v1/audit-log-streams", - dto + `/api/v1/audit-log-streams/${provider}`, + { ...params, provider } ); - return data; + + return data.auditLogStream; }, - onSuccess: (_, { orgId }) => { - queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list(orgId) }); - } + onSuccess: () => queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list() }) }); }; export const useUpdateAuditLogStream = () => { const queryClient = useQueryClient(); - - return useMutation<{ auditLogStream: TAuditLogStream }, object, TUpdateAuditLogStreamDTO>({ - mutationFn: async (dto) => { + return useMutation({ + mutationFn: async ({ auditLogStreamId, provider, ...params }: TUpdateAuditLogStreamDTO) => { const { data } = await apiRequest.patch<{ auditLogStream: TAuditLogStream }>( - `/api/v1/audit-log-streams/${dto.id}`, - dto + `/api/v1/audit-log-streams/${provider}/${auditLogStreamId}`, + params ); - return data; + + return data.auditLogStream; }, - onSuccess: (_, { orgId }) => { - queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list(orgId) }); + onSuccess: (_, { auditLogStreamId, provider }) => { + queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list() }); + queryClient.invalidateQueries({ + queryKey: auditLogStreamKeys.getById(provider, auditLogStreamId) + }); } }); }; export const useDeleteAuditLogStream = () => { const queryClient = useQueryClient(); - - return useMutation<{ auditLogStream: TAuditLogStream }, object, TDeleteAuditLogStreamDTO>({ - mutationFn: async (dto) => { + return useMutation({ + mutationFn: async ({ auditLogStreamId, provider }: TDeleteAuditLogStreamDTO) => { const { data } = await apiRequest.delete<{ auditLogStream: TAuditLogStream }>( - `/api/v1/audit-log-streams/${dto.id}` + `/api/v1/audit-log-streams/${provider}/${auditLogStreamId}` ); - return data; + + return data.auditLogStream; }, - onSuccess: (_, { orgId }) => { - queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list(orgId) }); + onSuccess: (_, { auditLogStreamId, provider }) => { + queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list() }); + queryClient.invalidateQueries({ + queryKey: auditLogStreamKeys.getById(provider, auditLogStreamId) + }); } }); }; diff --git a/frontend/src/hooks/api/auditLogStreams/queries.tsx b/frontend/src/hooks/api/auditLogStreams/queries.tsx index ff7d8b499..97d7d6267 100644 --- a/frontend/src/hooks/api/auditLogStreams/queries.tsx +++ b/frontend/src/hooks/api/auditLogStreams/queries.tsx @@ -1,40 +1,89 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TAuditLogStream } from "./types"; +import { TAuditLogStreamProviderOption } from "./types/provider-options"; +import { LogProvider } from "./enums"; +import { TAuditLogStream, TAuditLogStreamProviderMap } from "./types"; export const auditLogStreamKeys = { - list: (orgId: string) => ["audit-log-stream", { orgId }], - getById: (id: string) => ["audit-log-stream-details", { id }] + all: ["audit-log-stream"] as const, + options: () => [...auditLogStreamKeys.all, "options"] as const, + list: () => [...auditLogStreamKeys.all, "list"] as const, + getById: (provider: string, id: string) => + [...auditLogStreamKeys.all, provider, "get-by-id", id] as const }; -const fetchAuditLogStreams = async () => { - const { data } = await apiRequest.get<{ auditLogStreams: TAuditLogStream[] }>( - "/api/v1/audit-log-streams" - ); +export const useGetAuditLogStreamOptions = ( + options?: Omit< + UseQueryOptions< + TAuditLogStreamProviderOption[], + unknown, + TAuditLogStreamProviderOption[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: auditLogStreamKeys.options(), + queryFn: async () => { + const { data } = await apiRequest.get<{ providerOptions: TAuditLogStreamProviderOption[] }>( + "/api/v1/audit-log-streams/options" + ); - return data.auditLogStreams; -}; - -export const useGetAuditLogStreams = (orgId: string) => - useQuery({ - queryKey: auditLogStreamKeys.list(orgId), - queryFn: () => fetchAuditLogStreams(), - enabled: Boolean(orgId) + return data.providerOptions; + }, + ...options }); - -const fetchAuditLogStreamDetails = async (id: string) => { - const { data } = await apiRequest.get<{ auditLogStream: TAuditLogStream }>( - `/api/v1/audit-log-streams/${id}` - ); - - return data.auditLogStream; }; -export const useGetAuditLogStreamDetails = (id: string) => - useQuery({ - queryKey: auditLogStreamKeys.getById(id), - queryFn: () => fetchAuditLogStreamDetails(id), - enabled: Boolean(id) +export const useListAuditLogStreams = ( + options?: Omit< + UseQueryOptions< + TAuditLogStream[], + unknown, + TAuditLogStream[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: auditLogStreamKeys.list(), + queryFn: async () => { + const { data } = await apiRequest.get<{ auditLogStreams: TAuditLogStream[] }>( + "/api/v1/audit-log-streams" + ); + + return data.auditLogStreams; + }, + ...options }); +}; + +export const useGetAuditLogStreamById = ( + provider: T, + logStreamId: string, + options?: Omit< + UseQueryOptions< + TAuditLogStreamProviderMap[T], + unknown, + TAuditLogStreamProviderMap[T], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: auditLogStreamKeys.getById(provider, logStreamId), + queryFn: async () => { + const { data } = await apiRequest.get<{ auditLogStream: TAuditLogStreamProviderMap[T] }>( + `/api/v1/audit-log-streams/${provider}/${logStreamId}` + ); + + return data.auditLogStream; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/auditLogStreams/types.ts b/frontend/src/hooks/api/auditLogStreams/types.ts deleted file mode 100644 index 8e21a3209..000000000 --- a/frontend/src/hooks/api/auditLogStreams/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -export type LogStreamHeaders = { - key: string; - value: string; -}; - -export type TAuditLogStream = { - id: string; - url: string; - headers?: LogStreamHeaders[]; -}; - -export type TCreateAuditLogStreamDTO = { - url: string; - headers?: LogStreamHeaders[]; - orgId: string; -}; - -export type TUpdateAuditLogStreamDTO = { - id: string; - url?: string; - headers?: LogStreamHeaders[]; - orgId: string; -}; - -export type TDeleteAuditLogStreamDTO = { - id: string; - orgId: string; -}; diff --git a/frontend/src/hooks/api/auditLogStreams/types/index.ts b/frontend/src/hooks/api/auditLogStreams/types/index.ts new file mode 100644 index 000000000..d3dffe6a9 --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/types/index.ts @@ -0,0 +1,25 @@ +import { LogProvider } from "../enums"; +import { TCustomProviderLogStream } from "./providers/custom-provider"; +import { TDatadogProviderLogStream } from "./providers/datadog-provider"; +import { TSplunkProviderLogStream } from "./providers/splunk-provider"; + +export type TAuditLogStream = + | TCustomProviderLogStream + | TDatadogProviderLogStream + | TSplunkProviderLogStream; + +export type TAuditLogStreamProviderMap = { + [LogProvider.Custom]: TCustomProviderLogStream; + [LogProvider.Datadog]: TDatadogProviderLogStream; + [LogProvider.Splunk]: TSplunkProviderLogStream; +}; + +export type TCreateAuditLogStreamDTO = Pick; +export type TUpdateAuditLogStreamDTO = Pick & { + provider: LogProvider; + auditLogStreamId: string; +}; +export type TDeleteAuditLogStreamDTO = { + provider: LogProvider; + auditLogStreamId: string; +}; diff --git a/frontend/src/hooks/api/auditLogStreams/types/provider-options.ts b/frontend/src/hooks/api/auditLogStreams/types/provider-options.ts new file mode 100644 index 000000000..e9b06dfef --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/types/provider-options.ts @@ -0,0 +1,11 @@ +import { LogProvider } from "../enums"; + +export type TAuditLogStreamProviderOptionBase = { + name: string; +}; + +export type TAuditLogStreamProviderOption = { + [P in keyof typeof LogProvider]: TAuditLogStreamProviderOptionBase & { + provider: (typeof LogProvider)[P]; + }; +}[keyof typeof LogProvider]; diff --git a/frontend/src/hooks/api/auditLogStreams/types/providers/custom-provider.ts b/frontend/src/hooks/api/auditLogStreams/types/providers/custom-provider.ts new file mode 100644 index 000000000..83ff9e010 --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/types/providers/custom-provider.ts @@ -0,0 +1,10 @@ +import { LogProvider } from "../../enums"; +import { TRootProviderLogStream } from "./root-provider"; + +export type TCustomProviderLogStream = TRootProviderLogStream & { + provider: LogProvider.Custom; + credentials: { + url: string; + headers: { key: string; value: string }[]; + }; +}; diff --git a/frontend/src/hooks/api/auditLogStreams/types/providers/datadog-provider.ts b/frontend/src/hooks/api/auditLogStreams/types/providers/datadog-provider.ts new file mode 100644 index 000000000..85198bb1a --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/types/providers/datadog-provider.ts @@ -0,0 +1,10 @@ +import { LogProvider } from "../../enums"; +import { TRootProviderLogStream } from "./root-provider"; + +export type TDatadogProviderLogStream = TRootProviderLogStream & { + provider: LogProvider.Datadog; + credentials: { + url: string; + token: string; + }; +}; diff --git a/frontend/src/hooks/api/auditLogStreams/types/providers/root-provider.ts b/frontend/src/hooks/api/auditLogStreams/types/providers/root-provider.ts new file mode 100644 index 000000000..5ea3137e5 --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/types/providers/root-provider.ts @@ -0,0 +1,6 @@ +export type TRootProviderLogStream = { + id: string; + orgId: string; + createdAt: string; + updatedAt: string; +}; diff --git a/frontend/src/hooks/api/auditLogStreams/types/providers/splunk-provider.ts b/frontend/src/hooks/api/auditLogStreams/types/providers/splunk-provider.ts new file mode 100644 index 000000000..bf5c676ef --- /dev/null +++ b/frontend/src/hooks/api/auditLogStreams/types/providers/splunk-provider.ts @@ -0,0 +1,10 @@ +import { LogProvider } from "../../enums"; +import { TRootProviderLogStream } from "./root-provider"; + +export type TSplunkProviderLogStream = TRootProviderLogStream & { + provider: LogProvider.Splunk; + credentials: { + hostname: string; + token: string; + }; +}; diff --git a/frontend/src/layouts/OrganizationLayout/components/AuditLogBanner/AuditLogBanner.tsx b/frontend/src/layouts/OrganizationLayout/components/AuditLogBanner/AuditLogBanner.tsx index 9f7494076..755611587 100644 --- a/frontend/src/layouts/OrganizationLayout/components/AuditLogBanner/AuditLogBanner.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/AuditLogBanner/AuditLogBanner.tsx @@ -1,12 +1,10 @@ -import { useOrganization } from "@app/context"; -import { useFetchServerStatus, useGetAuditLogStreams } from "@app/hooks/api"; +import { useFetchServerStatus, useListAuditLogStreams } from "@app/hooks/api"; import { OrgAlertBanner } from "../OrgAlertBanner"; export const AuditLogBanner = () => { - const org = useOrganization(); const { data: status, isLoading: isLoadingStatus } = useFetchServerStatus(); - const { data: streams, isLoading: isLoadingStreams } = useGetAuditLogStreams(org.currentOrg.id); + const { data: streams, isLoading: isLoadingStreams } = useListAuditLogStreams(); if (isLoadingStreams || isLoadingStatus || !streams) return null; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm.tsx deleted file mode 100644 index 7e16afd60..000000000 --- a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { Controller, useFieldArray, useForm } from "react-hook-form"; -import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { z } from "zod"; - -import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, FormLabel, IconButton, Input, Spinner } from "@app/components/v2"; -import { useOrganization } from "@app/context"; -import { - useCreateAuditLogStream, - useGetAuditLogStreamDetails, - useUpdateAuditLogStream -} from "@app/hooks/api"; - -type Props = { - id?: string; - onClose: () => void; -}; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const formSchema = z.object({ - url: z.string().url().min(1), - headers: z - .object({ - key: z.string(), - value: z.string() - }) - .array() - .optional() -}); -type TForm = z.infer; - -export const AuditLogStreamForm = ({ id = "", onClose }: Props) => { - const isEdit = Boolean(id); - const { currentOrg } = useOrganization(); - const orgId = currentOrg?.id || ""; - - const auditLogStream = useGetAuditLogStreamDetails(id); - const createAuditLogStream = useCreateAuditLogStream(); - const updateAuditLogStream = useUpdateAuditLogStream(); - - const { - handleSubmit, - control, - setValue, - getValues, - formState: { isSubmitting } - } = useForm({ - values: auditLogStream?.data, - defaultValues: { - headers: [{ key: "", value: "" }] - } - }); - - const headerFields = useFieldArray({ - control, - name: "headers" - }); - - const handleAuditLogStreamEdit = async ({ headers, url }: TForm) => { - if (!id) return; - try { - await updateAuditLogStream.mutateAsync({ - id, - orgId, - headers, - url - }); - createNotification({ - type: "success", - text: "Successfully updated stream" - }); - onClose(); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to update stream" - }); - } - }; - - const handleFormSubmit = async ({ headers = [], url }: TForm) => { - if (isSubmitting) return; - const sanitizedHeaders = headers.filter(({ key, value }) => Boolean(key) && Boolean(value)); - const streamHeaders = sanitizedHeaders.length ? sanitizedHeaders : undefined; - if (isEdit) { - await handleAuditLogStreamEdit({ headers: streamHeaders, url }); - return; - } - try { - await createAuditLogStream.mutateAsync({ - orgId, - headers: streamHeaders, - url - }); - createNotification({ - type: "success", - text: "Successfully created stream" - }); - onClose(); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: (err as Error)?.message ?? "Failed to create stream" - }); - } - }; - - if (isEdit && auditLogStream.isPending) { - return ( -
- -
- ); - } - - return ( -
-
- ( - - - - )} - /> - - {headerFields.fields.map(({ id: headerFieldId }, i) => ( -
- ( - - - - )} - /> - ( - - - - )} - /> - { - const header = getValues("headers"); - if (header && header?.length > 1) { - headerFields.remove(i); - } else { - setValue("headers", [{ key: "", value: "" }]); - } - }} - > - - -
- ))} -
- -
-
-
- - -
-
- ); -}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/AuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/AuditLogStreamForm.tsx new file mode 100644 index 000000000..1af5e8d1a --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/AuditLogStreamForm.tsx @@ -0,0 +1,125 @@ +import { createNotification } from "@app/components/notifications"; +import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams"; +import { useCreateAuditLogStream, useUpdateAuditLogStream } from "@app/hooks/api"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { TAuditLogStream } from "@app/hooks/api/types"; +import { DiscriminativePick } from "@app/types"; + +import { AuditLogStreamHeader } from "../components/AuditLogStreamHeader"; +import { CustomProviderAuditLogStreamForm } from "./CustomProviderAuditLogStreamForm"; +import { DatadogProviderAuditLogStreamForm } from "./DatadogProviderAuditLogStreamForm"; +import { SplunkProviderAuditLogStreamForm } from "./SplunkProviderAuditLogStreamForm"; + +type FormProps = { + onComplete: (auditLogStream: TAuditLogStream) => void; +}; + +type CreateFormProps = FormProps & { provider: LogProvider }; +type UpdateFormProps = FormProps & { + auditLogStream: TAuditLogStream; +}; + +const CreateForm = ({ provider, onComplete }: CreateFormProps) => { + const createAuditLogStream = useCreateAuditLogStream(); + const { name: providerName } = AUDIT_LOG_STREAM_PROVIDER_MAP[provider]; + + const onSubmit = async ( + formData: DiscriminativePick + ) => { + try { + const logStream = await createAuditLogStream.mutateAsync(formData); + createNotification({ + text: `Successfully created ${providerName} Log Stream`, + type: "success" + }); + onComplete(logStream); + } catch (err: any) { + console.error(err); + createNotification({ + title: `Failed to create ${providerName} Log Stream`, + text: err.message, + type: "error" + }); + } + }; + + switch (provider) { + case LogProvider.Custom: + return ; + case LogProvider.Datadog: + return ; + case LogProvider.Splunk: + return ; + default: + throw new Error(`Unhandled Provider: ${provider}`); + } +}; + +const UpdateForm = ({ auditLogStream, onComplete }: UpdateFormProps) => { + const updateAuditLogStream = useUpdateAuditLogStream(); + const { name: providerName } = AUDIT_LOG_STREAM_PROVIDER_MAP[auditLogStream.provider]; + + const onSubmit = async ( + formData: DiscriminativePick + ) => { + try { + const connection = await updateAuditLogStream.mutateAsync({ + auditLogStreamId: auditLogStream.id, + ...formData + }); + createNotification({ + text: `Successfully updated ${providerName} Log Stream`, + type: "success" + }); + onComplete(connection); + } catch (err: any) { + console.error(err); + createNotification({ + title: `Failed to update ${providerName} Log Stream`, + text: err.message, + type: "error" + }); + } + }; + + switch (auditLogStream.provider) { + case LogProvider.Custom: + return ( + + ); + case LogProvider.Datadog: + return ( + + ); + case LogProvider.Splunk: + return ( + + ); + default: + throw new Error(`Unhandled Provider: ${(auditLogStream as TAuditLogStream).provider}`); + } +}; + +type Props = { onBack?: () => void } & Pick & + ( + | { provider: LogProvider; auditLogStream?: undefined } + | { provider?: undefined; auditLogStream: TAuditLogStream } + ); +export const AuditLogStreamForm = ({ onBack, ...props }: Props) => { + const { provider, auditLogStream } = props; + + return ( +
+ + {auditLogStream ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx new file mode 100644 index 000000000..e0dd68155 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/CustomProviderAuditLogStreamForm.tsx @@ -0,0 +1,154 @@ +import { Controller, FormProvider, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, FormLabel, IconButton, Input, ModalClose } from "@app/components/v2"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { TCustomProviderLogStream } from "@app/hooks/api/auditLogStreams/types/providers/custom-provider"; + +type Props = { + auditLogStream?: TCustomProviderLogStream; + onSubmit: (formData: FormData) => void; +}; + +const formSchema = z.object({ + provider: z.literal(LogProvider.Custom), + credentials: z.object({ + url: z.string().url().trim().min(1).max(255), + headers: z + .object({ + key: z.string().min(1), + value: z.string().min(1) + }) + .array() + }) +}); + +type FormData = z.infer; + +export const CustomProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: Props) => { + const isUpdate = Boolean(auditLogStream); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: auditLogStream ?? { + provider: LogProvider.Custom + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty }, + getValues, + setValue + } = form; + + const headerFields = useFieldArray({ + control, + name: "credentials.headers" + }); + + return ( + +
+ ( + + + + )} + /> + + + {headerFields.fields.map(({ id: headerFieldId }, i) => ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + { + const header = getValues("credentials.headers"); + if (header && header?.length > 1) { + headerFields.remove(i); + } else { + setValue("credentials.headers", [{ key: "", value: "" }]); + } + }} + > + + +
+ ))} +
+ +
+ +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/DatadogProviderAuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/DatadogProviderAuditLogStreamForm.tsx new file mode 100644 index 000000000..8ed339446 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/DatadogProviderAuditLogStreamForm.tsx @@ -0,0 +1,132 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { TDatadogProviderLogStream } from "@app/hooks/api/auditLogStreams/types/providers/datadog-provider"; + +type Props = { + auditLogStream?: TDatadogProviderLogStream; + onSubmit: (formData: FormData) => void; +}; + +const formSchema = z.object({ + provider: z.literal(LogProvider.Datadog), + credentials: z.object({ + url: z.string().url().trim().min(1).max(255), + token: z + .string() + .trim() + .regex(/^[a-fA-F0-9]{32}$/, "Invalid Datadog API key format") + }) +}); + +type FormData = z.infer; + +const DATADOG_ENDPOINTS = { + "Datadog US1": "https://http-intake.logs.datadoghq.com/api/v2/logs", + "Datadog US3": "https://http-intake.logs.us3.datadoghq.com/api/v2/logs", + "Datadog US5": "https://http-intake.logs.us5.datadoghq.com/api/v2/logs", + "Datadog EU": "https://http-intake.logs.datadoghq.eu/api/v2/logs", + "Datadog AP1": "https://http-intake.logs.ap1.datadoghq.com/api/v2/logs", + "Datadog AP2": "https://http-intake.logs.ap2.datadoghq.com/api/v2/logs", + "Datadog GovCloud (US1-FED)": "https://http-intake.logs.ddog-gov.com/api/v2/logs" +}; + +export const DatadogProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: Props) => { + const isUpdate = Boolean(auditLogStream); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: auditLogStream ?? { + provider: LogProvider.Datadog, + credentials: { + url: DATADOG_ENDPOINTS["Datadog US1"] + } + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/SplunkProviderAuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/SplunkProviderAuditLogStreamForm.tsx new file mode 100644 index 000000000..c9d248d61 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/SplunkProviderAuditLogStreamForm.tsx @@ -0,0 +1,120 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, Input, ModalClose, SecretInput } from "@app/components/v2"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { TSplunkProviderLogStream } from "@app/hooks/api/auditLogStreams/types/providers/splunk-provider"; + +type Props = { + auditLogStream?: TSplunkProviderLogStream; + onSubmit: (formData: FormData) => void; +}; + +const formSchema = z.object({ + provider: z.literal(LogProvider.Splunk), + credentials: z.object({ + hostname: z + .string() + .trim() + .min(1) + .max(255) + .superRefine((val, ctx) => { + if (val.includes("://")) { + ctx.addIssue({ + code: "custom", + message: "Hostname should not include protocol" + }); + return; + } + + try { + const url = new URL(`https://${val}`); + if (url.hostname !== val) { + ctx.addIssue({ + code: "custom", + message: "Must be a valid hostname without port or path" + }); + } + } catch { + ctx.addIssue({ code: "custom", message: "Invalid hostname" }); + } + }), + token: z.string().uuid().trim().min(1) + }) +}); + +type FormData = z.infer; + +export const SplunkProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: Props) => { + const isUpdate = Boolean(auditLogStream); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: auditLogStream ?? { + provider: LogProvider.Splunk + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamTab.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamTab.tsx index beb0f63db..b58d1a6eb 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamTab.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamTab.tsx @@ -1,72 +1,24 @@ -import { faPlug, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; -import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; -import { - Button, - DeleteActionModal, - EmptyState, - Modal, - ModalContent, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - THead, - Tr -} from "@app/components/v2"; -import { - OrgPermissionActions, - OrgPermissionSubjects, - useOrganization, - useSubscription -} from "@app/context"; +import { Button } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import { withPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks"; -import { useDeleteAuditLogStream, useGetAuditLogStreams } from "@app/hooks/api"; -import { AuditLogStreamForm } from "./AuditLogStreamForm"; +import { AuditLogStreamTable } from "./components/AuditLogStreamTable"; +import { AddAuditLogStreamModal } from "./components"; export const AuditLogStreamsTab = withPermission( () => { - const { currentOrg } = useOrganization(); - const orgId = currentOrg?.id || ""; - const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ - "auditLogStreamForm", - "deleteAuditLogStream", - "upgradePlan" - ] as const); const { subscription } = useSubscription(); - const { data: auditLogStreams, isPending: isAuditLogStreamsLoading } = - useGetAuditLogStreams(orgId); - - // mutation - const { mutateAsync: deleteAuditLogStream } = useDeleteAuditLogStream(); - - const handleAuditLogStreamDelete = async () => { - try { - const auditLogStreamId = popUp?.deleteAuditLogStream?.data as string; - await deleteAuditLogStream({ - id: auditLogStreamId, - orgId - }); - handlePopUpClose("deleteAuditLogStream"); - createNotification({ - type: "success", - text: "Successfully deleted stream" - }); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to delete stream" - }); - } - }; + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "auditLogStreamForm", + "upgradePlan" + ] as const); return (
@@ -84,8 +36,10 @@ export const AuditLogStreamsTab = withPermission( }} leftIcon={} isDisabled={!isAllowed} + variant="outline_bg" + colorSchema="secondary" > - Create + Add Log Stream )} @@ -93,102 +47,15 @@ export const AuditLogStreamsTab = withPermission(

Send audit logs from Infisical to external logging providers via HTTP

-
- - - - - - - - - - {isAuditLogStreamsLoading && ( - - )} - {!isAuditLogStreamsLoading && auditLogStreams && auditLogStreams?.length === 0 && ( - - - - )} - {!isAuditLogStreamsLoading && - auditLogStreams?.map(({ id, url }) => ( - - - - - ))} - -
URLAction
- -
- {url} - -
- - {(isAllowed) => ( - - )} - - - {(isAllowed) => ( - - )} - -
-
-
-
- + { - handlePopUpToggle("auditLogStreamForm", isModalOpen); - }} - > - - handlePopUpToggle("auditLogStreamForm")} - /> - - + onOpenChange={(isOpen) => handlePopUpToggle("auditLogStreamForm", isOpen)} + /> handlePopUpToggle("upgradePlan", isOpen)} - text="You can add audit log streams if you switch to Infisical's Enterprise plan." - /> - handlePopUpToggle("deleteAuditLogStream", isOpen)} - onClose={() => handlePopUpClose("deleteAuditLogStream")} - onDeleteApproved={handleAuditLogStreamDelete} + text="You can add audit log streams if you switch to Infisical's Enterprise plan." />
); diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AddAuditLogStreamModal.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AddAuditLogStreamModal.tsx new file mode 100644 index 000000000..5a6892a49 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AddAuditLogStreamModal.tsx @@ -0,0 +1,69 @@ +import { Dispatch, SetStateAction, useState } from "react"; + +import { Modal, ModalContent } from "@app/components/v2"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { TAuditLogStream } from "@app/hooks/api/types"; + +import { AuditLogStreamForm } from "../AuditLogStreamForm/AuditLogStreamForm"; +import { LogStreamProviderSelect } from "./LogStreamProviderSelect"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +type ContentProps = { + onComplete: (auditLogStream: TAuditLogStream) => void; + selectedProvider: LogProvider | null; + setSelectedProvider: Dispatch>; +}; + +const Content = ({ onComplete, selectedProvider, setSelectedProvider }: ContentProps) => { + if (selectedProvider) { + return ( + setSelectedProvider(null)} + provider={selectedProvider} + /> + ); + } + + return ; +}; + +export const AddAuditLogStreamModal = ({ isOpen, onOpenChange }: Props) => { + const [selectedProvider, setSelectedProvider] = useState(null); + + return ( + { + onOpenChange(e); + if (!e) setSelectedProvider(null); + }} + > + + Select a log provider or{" "} + {" "} + to stream logs to. + + > + onOpenChange(false)} + selectedProvider={selectedProvider} + setSelectedProvider={setSelectedProvider} + /> + + + ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamHeader.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamHeader.tsx new file mode 100644 index 000000000..12185b869 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamHeader.tsx @@ -0,0 +1,54 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; + +type Props = { + provider: LogProvider; + logStreamExists: boolean; + onBack?: () => void; +}; + +export const AuditLogStreamHeader = ({ provider, logStreamExists, onBack }: Props) => { + const providerDetails = AUDIT_LOG_STREAM_PROVIDER_MAP[provider]; + + return ( +
+
+ {providerDetails.image ? ( + {providerDetails.name} + ) : ( + providerDetails.icon && ( +
+ +
+ ) + )} +
+
+
{providerDetails.name}
+

+ {logStreamExists + ? `${providerDetails.name} Log Stream` + : `Create a ${providerDetails.name} Log Stream`} +

+
+ {onBack && ( + + )} +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamRow.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamRow.tsx new file mode 100644 index 000000000..037ba6f2a --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamRow.tsx @@ -0,0 +1,111 @@ +import { faAsterisk, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + IconButton, + Td, + Tooltip, + Tr +} from "@app/components/v2"; +import { OrgPermissionSubjects } from "@app/context"; +import { OrgPermissionActions } from "@app/context/OrgPermissionContext/types"; +import { AUDIT_LOG_STREAM_PROVIDER_MAP, getProviderUrl } from "@app/helpers/auditLogStreams"; +import { TAuditLogStream } from "@app/hooks/api/types"; + +type Props = { + logStream: TAuditLogStream; + onDelete: (logStream: TAuditLogStream) => void; + onEditCredentials: (logStream: TAuditLogStream) => void; +}; + +export const AuditLogStreamRow = ({ logStream, onDelete, onEditCredentials }: Props) => { + const { id, provider } = logStream; + + const providerDetails = AUDIT_LOG_STREAM_PROVIDER_MAP[provider]; + const url = getProviderUrl(logStream); + + return ( + + +
+
+ {providerDetails.image ? ( + {providerDetails.name} + ) : ( + providerDetails.icon && ( + + ) + )} +
+ {providerDetails.name} +
+ + +
+

{url}

+
+ + +
+ + + + + + + + + + {(isAllowed: boolean) => ( + } + onClick={() => onEditCredentials(logStream)} + > + Edit Credentials + + )} + + + {(isAllowed: boolean) => ( + } + onClick={() => onDelete(logStream)} + > + Delete Stream + + )} + + + + +
+ + + ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamTable.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamTable.tsx new file mode 100644 index 000000000..a9f739acc --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/AuditLogStreamTable.tsx @@ -0,0 +1,304 @@ +import { useMemo, useState } from "react"; +import { + faArrowDown, + faArrowUp, + faFilter, + faMagnifyingGlass, + faPlug, + faSearch +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + EmptyState, + IconButton, + Input, + Pagination, + Table, + TableContainer, + TableSkeleton, + TBody, + Th, + THead, + Tr +} from "@app/components/v2"; +import { AUDIT_LOG_STREAM_PROVIDER_MAP, getProviderUrl } from "@app/helpers/auditLogStreams"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; +import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; +import { useListAuditLogStreams } from "@app/hooks/api"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; +import { OrderByDirection } from "@app/hooks/api/generic/types"; +import { TAuditLogStream } from "@app/hooks/api/types"; + +import { AuditLogStreamRow } from "./AuditLogStreamRow"; +import { DeleteAuditLogStreamModal } from "./DeleteAuditLogStreamModal"; +import { EditAuditLogStreamCredentialsModal } from "./EditAuditLogStreamCredentialsModal"; + +enum LogStreamsOrderBy { + Provider = "provider", + Url = "url" +} + +type LogStreamFilters = { + providers: LogProvider[]; +}; + +export const AuditLogStreamTable = () => { + const { isPending, data: logStreams = [] } = useListAuditLogStreams(); + + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "delete", + "editCredentials" + ] as const); + + const [filters, setFilters] = useState({ + providers: [] + }); + + const { + search, + setSearch, + setPage, + page, + perPage, + setPerPage, + offset, + orderDirection, + toggleOrderDirection, + orderBy, + setOrderDirection, + setOrderBy + } = usePagination(LogStreamsOrderBy.Provider, { + initPerPage: getUserTablePreference("logStreamsTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("logStreamsTable", PreferenceKey.PerPage, newPerPage); + }; + + const filteredLogStreams = useMemo( + () => + logStreams + .filter((stream) => { + const { provider } = stream; + + if (filters.providers.length && !filters.providers.includes(provider)) return false; + + const searchValue = search.trim().toLowerCase(); + + return AUDIT_LOG_STREAM_PROVIDER_MAP[provider].name.toLowerCase().includes(searchValue); + }) + .sort((a, b) => { + const [one, two] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a]; + + switch (orderBy) { + case LogStreamsOrderBy.Url: + return getProviderUrl(one) + .toLowerCase() + .localeCompare(getProviderUrl(two).toLowerCase()); + case LogStreamsOrderBy.Provider: + default: + return AUDIT_LOG_STREAM_PROVIDER_MAP[one.provider].name + .toLowerCase() + .localeCompare(AUDIT_LOG_STREAM_PROVIDER_MAP[two.provider].name.toLowerCase()); + } + }), + [logStreams, orderDirection, search, orderBy, filters] + ); + + useResetPageHelper({ + totalCount: filteredLogStreams.length, + offset, + setPage + }); + + const handleSort = (column: LogStreamsOrderBy) => { + if (column === orderBy) { + toggleOrderDirection(); + return; + } + + setOrderBy(column); + setOrderDirection(OrderByDirection.ASC); + }; + + const getClassName = (col: LogStreamsOrderBy) => + twMerge("ml-2", orderBy === col ? "" : "opacity-30"); + + const getColSortIcon = (col: LogStreamsOrderBy) => + orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown; + + const isTableFiltered = Boolean(filters.providers.length); + + const handleDelete = (logStream: TAuditLogStream) => handlePopUpOpen("delete", logStream); + + const handleEditCredentials = (logStream: TAuditLogStream) => { + handlePopUpOpen("editCredentials", logStream); + }; + + return ( +
+
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search audit log streams..." + className="flex-1" + /> + + + + + + + + Filter by Provider + {logStreams.length ? ( + [...new Set(logStreams.map(({ provider }) => provider))].map((provider) => { + const providerDetails = AUDIT_LOG_STREAM_PROVIDER_MAP[provider]; + + return ( + { + e.preventDefault(); + setFilters((prev) => ({ + ...prev, + providers: prev.providers.includes(provider) + ? prev.providers.filter((a) => a !== provider) + : [...prev.providers, provider] + })); + }} + key={provider} + iconPos="right" + > +
+ {providerDetails.image ? ( + {providerDetails.name} + ) : ( + providerDetails.icon && ( + + ) + )} + {providerDetails.name} +
+
+ ); + }) + ) : ( + No Providers Configured + )} +
+
+
+ + + + + + + + + + + {isPending && ( + + )} + {filteredLogStreams.slice(offset, perPage * page).map((stream) => ( + + ))} + +
+
+ Provider + handleSort(LogStreamsOrderBy.Provider)} + > + + +
+
+
+ Endpoint URL + handleSort(LogStreamsOrderBy.Url)} + > + + +
+
+
+ {Boolean(filteredLogStreams.length) && ( + + )} + {!isPending && !filteredLogStreams?.length && ( + + )} +
+ handlePopUpToggle("delete", isOpen)} + auditLogStream={popUp.delete.data} + /> + handlePopUpToggle("editCredentials", isOpen)} + auditLogStream={popUp.editCredentials.data} + /> +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/DeleteAuditLogStreamModal.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/DeleteAuditLogStreamModal.tsx new file mode 100644 index 000000000..1a65e6c67 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/DeleteAuditLogStreamModal.tsx @@ -0,0 +1,54 @@ +import { createNotification } from "@app/components/notifications"; +import { DeleteActionModal } from "@app/components/v2"; +import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams"; +import { useDeleteAuditLogStream } from "@app/hooks/api"; +import { TAuditLogStream } from "@app/hooks/api/types"; + +type Props = { + auditLogStream?: TAuditLogStream; + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +export const DeleteAuditLogStreamModal = ({ isOpen, onOpenChange, auditLogStream }: Props) => { + const deleteAuditLogStream = useDeleteAuditLogStream(); + + if (!auditLogStream) return null; + + const { id: auditLogStreamId, provider } = auditLogStream; + + const providerDetails = AUDIT_LOG_STREAM_PROVIDER_MAP[provider]; + + const handleDelete = async () => { + try { + await deleteAuditLogStream.mutateAsync({ + auditLogStreamId, + provider + }); + + createNotification({ + text: `Successfully deleted ${providerDetails.name} stream`, + type: "success" + }); + + onOpenChange(false); + } catch (err) { + console.error(err); + + createNotification({ + text: `Failed to delete ${providerDetails.name} stream`, + type: "error" + }); + } + }; + + return ( + + ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/EditAuditLogStreamCredentialsModal.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/EditAuditLogStreamCredentialsModal.tsx new file mode 100644 index 000000000..6e21b10fe --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/EditAuditLogStreamCredentialsModal.tsx @@ -0,0 +1,34 @@ +import { Modal, ModalContent } from "@app/components/v2"; +import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams"; +import { TAuditLogStream } from "@app/hooks/api/types"; + +import { AuditLogStreamForm } from "../AuditLogStreamForm/AuditLogStreamForm"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + auditLogStream?: TAuditLogStream; +}; + +export const EditAuditLogStreamCredentialsModal = ({ + isOpen, + onOpenChange, + auditLogStream +}: Props) => { + if (!auditLogStream) return null; + + return ( + + + onOpenChange(false)} + auditLogStream={auditLogStream} + /> + + + ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/LogStreamProviderSelect.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/LogStreamProviderSelect.tsx new file mode 100644 index 000000000..af81cb9ca --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/LogStreamProviderSelect.tsx @@ -0,0 +1,155 @@ +import { useMemo } from "react"; +import { faSearch } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { EmptyState, Spinner } from "@app/components/v2"; +import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams"; +import { usePagination, useResetPageHelper } from "@app/hooks"; +import { useGetAuditLogStreamOptions } from "@app/hooks/api"; +import { LogProvider } from "@app/hooks/api/auditLogStreams/enums"; + +type Props = { + onSelect: (provider: LogProvider) => void; +}; + +// TODO: When we have more than 1 page of providers, uncomment the search components + +export const LogStreamProviderSelect = ({ onSelect }: Props) => { + const { isPending, data: logStreamOptions } = useGetAuditLogStreamOptions(); + + const { search, setPage, page, perPage, offset } = usePagination("", { + initPerPage: 16 + }); + + const filteredOptions = useMemo( + () => + (logStreamOptions || []) + .filter( + ({ name, provider }) => + name.toLowerCase().includes(search.trim().toLowerCase()) || + provider.toLowerCase().includes(search.trim().toLowerCase()) + ) + .sort((a, b) => { + if (a.provider === LogProvider.Custom) return 1; + if (b.provider === LogProvider.Custom) return -1; + return 0; + }), + [logStreamOptions, search] + ); + + useResetPageHelper({ + totalCount: filteredOptions.length, + offset, + setPage + }); + + if (isPending) { + return ( +
+ +

Loading options...

+
+ ); + } + + return ( +
+ {/* setSearch(e.target.value)} + leftIcon={} + placeholder="Search options..." + className="bg-mineshaft-800 placeholder:text-mineshaft-400" + /> */} +
+ {filteredOptions.slice(offset, perPage * page)?.map((option) => { + const { image, icon, name, size = 50 } = AUDIT_LOG_STREAM_PROVIDER_MAP[option.provider]; + + return ( + + ); + })} + {!filteredOptions?.length && ( + + )} +
+ {/* {Boolean(filteredOptions.length) && ( + +

Infisical is constantly adding support for more providers.

+

+ {`If you don't see the third-party + provider you're looking for,`}{" "} + + let us know on Slack + {" "} + or{" "} + + make a request on GitHub + + . +

+ + } + > +
+ + Don't see the third-party provider you're looking for? + + +
+ + } + count={filteredOptions.length} + page={page} + perPage={perPage} + onChangePage={setPage} + onChangePerPage={setPerPage} + perPageList={[16]} + /> + )} */} +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/index.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/index.tsx new file mode 100644 index 000000000..69febb4a9 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/components/index.tsx @@ -0,0 +1 @@ +export * from "./AddAuditLogStreamModal"; From 0f5e451f1a991d50983cdd6376c158b21ca96308 Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 4 Sep 2025 22:50:11 -0400 Subject: [PATCH 04/10] Wording tweak --- .../AuditLogStreamForm/DatadogProviderAuditLogStreamForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/DatadogProviderAuditLogStreamForm.tsx b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/DatadogProviderAuditLogStreamForm.tsx index 8ed339446..2db315416 100644 --- a/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/DatadogProviderAuditLogStreamForm.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/AuditLogStreamTab/AuditLogStreamForm/DatadogProviderAuditLogStreamForm.tsx @@ -70,7 +70,7 @@ export const DatadogProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: