import { UserSWGProfile, SwgStatus, SWGRootCertificate, RuleWeb, RuleBypass, RuleBypassInternalResources, UserSWGProfileMetadata, RuleWebMetadata, RuleBypassMetadata, SwgStatusMetadata, GroupMetadata, SWGRootCertificateMetadata, RuleBypassInternalResourcesMetadata, } from '@p81-common/p81-mongo-validation-schemas'; import { ISwgProfileRepository, IRedisManager, IOptions, IQuery, UserSWGExtendedProfile, IGroupWithId, TenantStatus, } from '@libs/types/'; import SwgActivationStatus from '@libs/enums/SwgActivationStatus'; import ActionType from '@libs/enums/ActionType'; import CategoriesService from '@libs/services/CategoriesService'; import { IDb, ICollection } from '@p81-common/p81-mongo-client'; import { IConfig } from 'config'; import { FireflyConverter } from '@p81-common/firefly-converter'; import { Logger } from '@libs/types/Logger'; import TpStatus from '@libs/types/TpStatus'; import ApplicationControlService from '@libs/services/ApplicationControlService'; export default class SwgProfileRepository implements ISwgProfileRepository { db: IDb; UserSWGProfile!: ICollection; RuleWeb!: ICollection; RuleBypass!: ICollection; SwgStatus!: ICollection; Group!: ICollection; SWGRootCertificate!: ICollection; RuleBypassInternalResources!: ICollection; categoriesService: CategoriesService; applicationControlService: ApplicationControlService; redisManager: IRedisManager; config: IConfig; fireflyConverter: FireflyConverter; constructor( config: IConfig, db: IDb, redisManager: IRedisManager, protected logger: Logger, ) { this.db = db; this.categoriesService = new CategoriesService(db); this.applicationControlService = new ApplicationControlService(db); this.UserSWGProfile = this.db.collection(UserSWGProfileMetadata); this.RuleWeb = this.db.collection(RuleWebMetadata); this.RuleBypass = this.db.collection(RuleBypassMetadata); this.SwgStatus = this.db.collection(SwgStatusMetadata); this.Group = this.db.collection(GroupMetadata); this.SWGRootCertificate = this.db.collection(SWGRootCertificateMetadata); this.RuleBypassInternalResources = this.db.collection( RuleBypassInternalResourcesMetadata, ); this.redisManager = redisManager; this.config = config; this.fireflyConverter = new FireflyConverter(db, logger); } private async getSwgStatus(tenantId: string): Promise { const status: TenantStatus = { webRuleDefaultAction: ActionType.ALLOW, swgStatus: SwgActivationStatus.INACTIVE, }; const oldStatus: SwgStatus | null = await this.SwgStatus.findOne({ tenantId }); if (oldStatus) { if (oldStatus.webRuleDefaultAction) { status.webRuleDefaultAction = oldStatus.webRuleDefaultAction as ActionType; } if (oldStatus.swgStatus) { status.swgStatus = oldStatus.swgStatus as SwgActivationStatus; } } return status; } private async getActiveCertificate(query: IQuery): Promise { const { tenantId } = query; const certificate: SWGRootCertificate | null = await this.SWGRootCertificate.findOne( { tenantId, active: true }, { projection: { serialNumber: 1 } }, ); return certificate; } /** * get tenant rules when user profile not found * @param query userId and tenant id to query on * @private */ private async getTenantRules(query: IQuery): Promise { this.logger.warn({ message: 'User profile not found, generate default one', query }); const { tenantId, userId } = query; const [webRules, bypassRules, status, groupsOfUser] = await Promise.all([ this.RuleWeb.find({ tenantId }).toArray(), this.RuleBypass.find({ tenantId }).toArray(), this.getSwgStatus(tenantId), (await this.Group.distinct('_id', { tenantId, users: { $in: [userId] }, })) as string[], ]); const response = { tenantId, userId, groupsOfUser, swgStatus: status.swgStatus, webRuleDefaultAction: status.webRuleDefaultAction, 'rewarn period': 43200, bypassRules, webRules, threatPreventionStatus: TpStatus.default, }; return response; } private async getProfileByUserId(query: IQuery): Promise { const { tenantId, userId } = query; const profile: UserSWGExtendedProfile[] = await this.UserSWGProfile.aggregate([ { $match: { tenantId, userId } }, { $lookup: { from: 'RuleWeb', localField: 'webRules', foreignField: '_id', as: 'webRules', pipeline: [ { $addFields: { id: '$_id', objectId: '$_id', createdAt: '$_created_at', className: 'RuleWeb', }, }, { $project: { _id: 0, _created_at: 0, }, }, ], }, }, { $lookup: { from: 'RuleBypass', localField: 'bypassRules', foreignField: '_id', as: 'bypassRules', pipeline: [ { $addFields: { id: '$_id', objectId: '$_id', createdAt: '$_created_at', className: 'RuleBypass', }, }, { $project: { _id: 0, _created_at: 0, }, }, ], }, }, { $lookup: { from: 'ThreatPreventionProfile', localField: 'threatPreventionProfile', foreignField: '_id', as: 'threatPreventionProfile', pipeline: [ { $addFields: { id: '$_id', }, }, { $project: { _id: 0, }, }, ], }, }, { $unwind: { path: '$threatPreventionProfile', preserveNullAndEmptyArrays: true } }, ]).toArray(); return profile[0] ?? this.getTenantRules(query); } private async prepareRules(rules: RuleWeb[] | RuleBypass[]) { const processingPromises: Promise[] = []; for (const rule of rules) { for (const destination of rule.destinations) { switch (destination.type) { case this.config.get('sdtTypeCategories'): destination.value = await this.categoriesService.findByIds( // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion destination.value as string[], ); break; case this.config.get('sdtTypeAppCtrlApplications'): { processingPromises.push( this.applicationControlService // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion .getBctiAppIdsByAppIds(destination.value as string[], this.logger) .then((apps) => { destination.value = apps; }), ); break; } default: break; } } } // Await all processing promises concurrently await Promise.all(processingPromises); } private async addInternalResourcesBypassRule(bypassRules: RuleBypass[], tenantId: string) { const rules: RuleBypassInternalResources[] | null = await this.RuleBypassInternalResources.find( {}, ).toArray(); let priority = bypassRules.length; for (const rule of rules) { const bypassRule: RuleBypass = { ...rule, tenantId, priority, }; bypassRules.push(bypassRule); priority++; } } public async fetchProfile( query: IQuery, options: IOptions | undefined, ): Promise { try { // eslint-disable-next-line prefer-const let [profile, certificate, ffKeyFireflyEnabled] = await Promise.all([ this.getProfileByUserId(query), this.getActiveCertificate(query), this.redisManager.hasFeatureFlag(query.tenantId, this.config.get('swgFirefly')), ]); const fireFlyOptions = { schemaVersion: process.env.ENFORCE_SCHEMA_VERSION, templateVersion: process.env.ENFORCE_TEMPLATE_VERSION, normalizedAppVersion: query.normalizedAppVersion, platform: query.platform, }; if (certificate?.serialNumber) { profile.swgRootCASerial = certificate.serialNumber; } profile.ffKeyFireflyEnabled = !!ffKeyFireflyEnabled; await this.prepareRules(profile?.webRules ?? []); await this.prepareRules(profile?.bypassRules ?? []); await this.addInternalResourcesBypassRule(profile?.bypassRules ?? [], query.tenantId); if (profile.ffKeyFireflyEnabled) { try { const { tenantId, userId, webRuleDefaultAction, bypassRules, webRules, threatPreventionProfile, threatPreventionStatus, } = profile; profile.fireflyProfile = await this.fireflyConverter.convertSwgProfile( { tenantId, userId, webRuleDefaultAction, bypassRules: bypassRules ?? [], webRules: webRules ?? [], threatPreventionStatus, threatPreventionProfile, }, fireFlyOptions, ); profile = { ...profile, ...this.fireflyConverter.handleSchemasForOlderVersions(profile, fireFlyOptions), }; } catch (err) { options?.logger.error({ message: 'Failed to generate firefly policy', err }); profile.fireflyProfile = []; } } return profile; } catch (err) { options?.logger?.error({ message: 'Failed fetching SWG profile', err }); throw err; } } }