import { Campaign, ChoiceRequest, DVHashes } from "../../models"; import { StepResult } from "../stepResult"; import { DVCheckCmd, DVCheckResult, DVError, FraudType, Type } from "../../models/dvFraud.model"; import { DeviceOS, getDeviceOS } from "../../utils/RequestUtils"; import logger from "../../utils/logger"; import constants from "../../utils/constants"; import { AbstractStep } from "../abstractStep"; import { CampaignModel } from "../../models/CampaignModel"; import { DVFraudService } from "../../services/dvFraud.service"; import MetricService, { BucketSet } from "../../services/metric.service"; import { CategoryProviderType } from "../../categories/categoryProvider"; const DV_STRONG_MISSING_UA_COUNTER = "dv_strong_missing_user_agent"; class DVFraudStep extends AbstractStep { private readonly metricService: MetricService; constructor( private readonly campaignModel: CampaignModel, private readonly dvFraudService: DVFraudService ) { super("dv_fraud", ["bypass", "is_fraud", "device_source", "fraud_type"]); this.metricBucket = BucketSet.zeroTo25ms; this.metricService = MetricService.getInstance(); this.metricService.addCounter( DV_STRONG_MISSING_UA_COUNTER, "request count without user agent", ["sdk_version"] ); } async run(request: ChoiceRequest): Promise { const result: StepResult = new StepResult(); if (this.isBypassing(request)) { result.passCampaigns(...request.campaigns); result.setMetricLabel("bypass", true); return result; } // temp this.checkUserAgent(request); const fraud = await this.isFraud(request, result); const isFraud = fraud.app || fraud.device; result.setMetricLabel("is_fraud", isFraud); if (isFraud) { result.passCampaigns( ...request.campaigns.filter((campaign) => { const filterApp = fraud.app && CampaignModel.hasPrebid( request.campaignData[campaign.id], "publisher-fraud", CategoryProviderType.DV ); const filterDevice = fraud.device && CampaignModel.hasPrebid( request.campaignData[campaign.id], "device-fraud", CategoryProviderType.DV ); return !filterApp && !filterDevice; }) ); return result; } else if (this.isDVStrictConditions(request)) { result.passCampaigns(...request.campaigns); } else { const campaigns: Campaign[] = request.campaigns.filter( (c) => !this.campaignModel.isDVStrongFiltering(request.campaignData[c.id]) || !CampaignModel.hasPrebid( request.campaignData[c.id], "device-fraud", CategoryProviderType.DV ) ); result.passCampaigns(...campaigns); } return result; } private isBypassing(request: ChoiceRequest): boolean { const bypassingApiKeys: string[] = this.featureConfigService.getParameterValueAsStringList( request, "ms-choices.dv-bypass-apikeys", ",", [] ); const apiKey = request.app?.app_api_key?.toString(); if (apiKey && bypassingApiKeys.includes(apiKey)) { logger.debug(() => `[DV FRAUD] apikey ${apiKey} bypasses DV fraud step`); return true; } return false; } // temp: add metrics for missing user agent when at least one campaign is dv strong private checkUserAgent(request: ChoiceRequest) { if ( !request.device?.ua && request.campaigns.some((c) => this.campaignModel.isDVStrongFiltering(request.campaignData[c.id]) ) ) { this.metricService.incCounterWithReq(request, DV_STRONG_MISSING_UA_COUNTER, 1, { sdk_version: request.app?.sdk_version, }); } } private async isFraud( request: ChoiceRequest, result: StepResult ): Promise<{ app: boolean; device: boolean; }> { // get/check required fields const deviceOS: DeviceOS = getDeviceOS(request.device); result.setMetricLabel("device_source", deviceOS ? deviceOS : "UNKNOWN"); logger.debug(() => `[DV FRAUD] checking request, device_source: ${deviceOS}`); const deviceError = DVFraudStep.checkIp(request); if (deviceError) { logger.error(new Error(`[DV FRAUD] ${deviceError.code}`)); result.error = { code: deviceError.code }; return { app: true, device: true }; } // create check commands const checkCmds: DVCheckCmd[] = []; // add app related commands checkCmds.push(...this.getAppCommands(request, deviceOS)); // add device related commands checkCmds.push(...this.getDeviceCmds(request)); const dvResults: DVCheckResult[] = await Promise.all( checkCmds.map((cmd) => this.dvFraudService.isBlacklisted(cmd)) ); // add hashes in request const dvHashes: DVHashes = {}; dvResults.forEach((dvRes) => (dvHashes[dvRes.fraudType.toString()] = dvRes.hash.decimal)); request.dvHashes = dvHashes; const returnValue = { app: false, device: false }; // handle result for (const fraudResult of dvResults) { // ignore inactive fraud types if (fraudResult.isFraud && this.dvFraudService.isActiveFraudType(fraudResult.fraudType)) { const fraudType = fraudResult.fraudType; result.setMetricLabel("fraud_type", fraudType); switch (fraudType) { case FraudType.APP_BUNDLE: case FraudType.DOMAIN: case FraudType.SUBDOMAIN: returnValue.app = true; break; case FraudType.IP: case FraudType.IP_UA: returnValue.device = true; } } } logger.debug( () => `[DV FRAUD] device_source: ${deviceOS}, device_fraud: ${returnValue.device}, app_fraud: ${returnValue.app}` ); return returnValue; } private getAppCommands(request: ChoiceRequest, deviceOs: DeviceOS): DVCheckCmd[] { const cmds: DVCheckCmd[] = []; if (request.isWeb) { cmds.push(...this.dvFraudService.getWebCmds(request.app?.bundle_id)); } else if (deviceOs === "android") { cmds.push(this.dvFraudService.getAppCmd(request.app?.bundle_id)); } else if (deviceOs === "ios") { cmds.push(this.dvFraudService.getAppCmd(request.app?.app_id)); } for (const c of cmds) { if (!c.toCheck) { c.toCheck = ""; } else { c.toCheck = c.toCheck.toLowerCase(); } } return cmds; } private static checkIp(request: ChoiceRequest): DVError | null { request.device ??= {}; let ip = request.device?.ip; request.device.ipVersion = 4; if (!ip || ip === "") { ip = request.device.ipv6; request.device.ipVersion = 6; } if (!ip) { return { code: "no_ip_found" }; } request.device.ip = ip; return null; } /** * Check if request and redis condition allows us to perform a strict DV check. * @param {ChoiceRequest} request * @private */ private isDVStrictConditions(request: ChoiceRequest): boolean { const hasUA: boolean = !!request.device?.ua; const redisClientConnected: boolean = this.dvFraudService.isRedisConnected(); const dvStrict: boolean = hasUA && redisClientConnected; if (!dvStrict) { logger.debug( () => `[DV FRAUD] request is not 'dv strict', redis client connected: ${redisClientConnected}, has user-agent: ${hasUA}` ); } return dvStrict; } private getDeviceCmds(request: ChoiceRequest): DVCheckCmd[] { const checkCmds: DVCheckCmd[] = []; const ip = request.device?.ip; let ua = request.device?.ua; // clean user agent if needed : User agent from some SSP (Double click) needs to be cleaned, ",gzip(gfe)" should be removed if (ua && ua.endsWith(constants.DV_GFE_SUFFIX)) { ua = ua.substring(0, ua.length - constants.DV_GFE_SUFFIX.length); } // check ip + ua, or ip only if (ua) { checkCmds.push({ fraudType: FraudType.IP_UA, type: Type.Device, toCheck: ip + ua, }); } checkCmds.push({ fraudType: FraudType.IP, type: Type.Device, toCheck: ip }); return checkCmds; } } export default DVFraudStep;