lib/liveValidation/liveValidator.ts (954 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. import * as http from "http"; import * as os from "os"; import * as path from "path"; import { resolve as pathResolve } from "path"; import { ParsedUrlQuery } from "querystring"; import * as util from "util"; import { URL } from "url"; import * as _ from "lodash"; import { diffRequestResponse } from "../armValidator/roundTripValidator"; import * as models from "../models"; import { requestResponseDefinition } from "../models/requestResponse"; import { LowerHttpMethods, SwaggerSpec } from "../swagger/swaggerTypes"; import { SchemaValidateFunction, SchemaValidateIssue, SchemaValidator, } from "../swaggerValidator/schemaValidator"; import * as C from "../util/constants"; import { log } from "../util/logging"; import * as utils from "../util/utils"; import { RuntimeException } from "../util/validationError"; import { inversifyGetContainer, inversifyGetInstance, TYPES } from "../inversifyUtils"; import { setDefaultOpts } from "../swagger/loader"; import { apiValidationErrors, ApiValidationErrorCode } from "../util/errorDefinitions"; import { kvPairsToObject, getProviderFromPathTemplate, getProviderFromSpecPath, } from "../util/utils"; import { LiveValidatorLoader, LiveValidatorLoaderOption } from "./liveValidatorLoader"; import { OperationSearcher } from "./operationSearcher"; import { LiveRequest, LiveResponse, OperationContext, validateSwaggerLiveRequest, validateSwaggerLiveResponse, ValidationRequest, } from "./operationValidator"; const glob = require("glob"); export interface LiveValidatorOptions extends LiveValidatorLoaderOption { swaggerPaths: string[]; git: { shouldClone: boolean; url?: string; branch?: string; }; useRelativeSourceLocationUrl?: boolean; directory: string; swaggerPathsPattern: string[]; excludedSwaggerPathsPattern: string[]; isPathCaseSensitive: boolean; loadValidatorInBackground: boolean; loadValidatorInInitialize: boolean; enableRoundTripValidator?: boolean; isArmCall?: boolean; } export interface RequestResponsePair { readonly liveRequest: LiveRequest; readonly liveResponse: LiveResponse; } export interface LiveValidationResult { readonly isSuccessful?: boolean; readonly operationInfo: OperationContext; readonly errors: LiveValidationIssue[]; readonly runtimeException?: RuntimeException; } export interface RequestResponseLiveValidationResult { readonly requestValidationResult: LiveValidationResult; readonly responseValidationResult: LiveValidationResult; readonly runtimeException?: RuntimeException; } export type LiveValidationIssue = { code: ApiValidationErrorCode; pathsInPayload: string[]; resourceIds?: string[]; documentationUrl?: string; } & Omit<SchemaValidateIssue, "code">; /** * Additional data to log. */ interface Meta { [key: string]: any; } /** * Options for a validation operation. * If `includeErrors` is missing or empty, all error codes will be included. */ export interface ValidateOptions { readonly includeErrors?: ApiValidationErrorCode[]; readonly includeOperationMatch?: boolean; } export enum LiveValidatorLoggingLevels { error = "error", warn = "warn", info = "info", verbose = "verbose", debug = "debug", silly = "silly", } export enum LiveValidatorLoggingTypes { trace = "trace", perfTrace = "perfTrace", error = "error", incomingRequest = "incomingRequest", specTrace = "specTrace", } /** * @class * Live Validator for Azure swagger APIs. */ export class LiveValidator { public options: LiveValidatorOptions; public operationSearcher: OperationSearcher; public swaggerList: string[] = []; private logFunction?: (message: string, level: string, meta?: Meta) => void; private loader?: LiveValidatorLoader; private loadInBackgroundComplete: boolean = false; private validateRequestResponsePair?: SchemaValidateFunction; /** * Constructs LiveValidator based on provided options. * * @param {object} ops The configuration options. * @param {callback function} logCallback The callback logger. * * @returns CacheBuilder Returns the configured CacheBuilder object. */ public constructor( options?: Partial<LiveValidatorOptions>, logCallback?: (message: string, level: string, meta?: Meta) => void ) { const ops: Partial<LiveValidatorOptions> = options || {}; this.logFunction = logCallback; setDefaultOpts(ops, { swaggerPaths: [], excludedSwaggerPathsPattern: C.DefaultConfig.ExcludedSwaggerPathsPattern, directory: path.resolve(os.homedir(), "repo"), isPathCaseSensitive: false, loadValidatorInBackground: true, loadValidatorInInitialize: false, isArmCall: false, }); if (!ops.git) { ops.git = { url: "https://github.com/Azure/azure-rest-api-specs.git", shouldClone: false, }; } if (!ops.git.url) { ops.git.url = "https://github.com/Azure/azure-rest-api-specs.git"; } if (!ops.git.shouldClone) { ops.git.shouldClone = false; } this.options = ops as LiveValidatorOptions; this.logging(`Creating livevalidator with options:${JSON.stringify(this.options)}`); this.operationSearcher = new OperationSearcher(this.logging); } /** * Initializes the Live Validator. */ public async initialize(): Promise<void> { const startTime = Date.now(); // Clone github repository if required if (this.options.git.shouldClone && this.options.git.url) { const cloneStartTime = Date.now(); utils.gitClone(this.options.directory, this.options.git.url, this.options.git.branch); this.logging( `Clone spec repository ${this.options.git.url}, branch:${this.options.git.branch} in livevalidator.initialize` ); this.logging( `Clone spec repository ${this.options.git.url}, branch:${this.options.git.branch}`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.perfTrace, "Oav.liveValidator.initialize.gitclone", Date.now() - cloneStartTime ); } // Construct array of swagger paths to be used for building a cache this.logging("Get swagger path."); const swaggerPaths = await this.getSwaggerPaths(); const container = inversifyGetContainer(); this.loader = inversifyGetInstance(LiveValidatorLoader, { container, fileRoot: this.options.directory, ...this.options, loadSuppression: this.options.loadSuppression ?? Object.keys(apiValidationErrors), }); this.loader.logging = this.logging; // re-set the transform context after set the logging function this.loader.setTransformContext(); const schemaValidator = container.get(TYPES.schemaValidator) as SchemaValidator; this.validateRequestResponsePair = await schemaValidator.compileAsync( requestResponseDefinition ); const allSpecs: SwaggerSpec[] = []; while (swaggerPaths.length > 0) { const swaggerPath = swaggerPaths.shift()!; this.swaggerList.push(swaggerPath); const spec = await this.getSwaggerInitializer(this.loader!, swaggerPath); if (spec !== undefined) { allSpecs.push(spec); } } this.logging("Apply global transforms for all specs"); try { this.loader.transformLoadedSpecs(); } catch (e) { // keeps building validator if it fails to tranform specs coz global transformers catches the exceptions and continue other schema transformings; // this error will be reported in validator building or validation runtime. const errMsg = `Failed to transform loaded specs, detail error message:${ (e as any)?.message }.\nError stack:${(e as any)?.stack}`; this.logging( errMsg, LiveValidatorLoggingLevels.error, LiveValidatorLoggingTypes.specTrace, "Oav.liveValidator.initialize.transformLoadedSpecs" ); } if (this.options.loadValidatorInInitialize) { this.logging("Building validator in initialization time..."); let spec; while (allSpecs.length > 0) { try { spec = allSpecs.shift()!; const loadStart = Date.now(); await this.loader.buildAjvValidator(spec); const durationInMs = Date.now() - loadStart; this.logging( `Complete building validator for ${spec._filePath} in initialization time`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.perfTrace, "Oav.liveValidator.initialize.loader.buildAjvValidator", durationInMs ); this.logging( `Complete building validator for spec ${spec._filePath}`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.specTrace, "Oav.liveValidator.initialize.loader.buildAjvValidator", durationInMs, { providerNamespace: spec._providerNamespace ?? "unknown", apiVersion: spec.info.version, specName: spec._filePath, } ); } catch (e) { this.logging( `Failed to build validator for spec ${spec?._filePath}.\nErrorMessage:${ (e as any)?.message }.\nErrorStack:${(e as any)?.stack}`, LiveValidatorLoggingLevels.error, LiveValidatorLoggingTypes.specTrace, "Oav.liveValidator.initialize.loader.buildAjvValidator", undefined, { providerNamespace: spec?._providerNamespace ?? "unknown", apiVersion: spec?.info.version ?? "unknown", specName: spec?._filePath, } ); } } this.loader = undefined; } this.logging("Cache initialization complete."); const elapsedTime = Date.now() - startTime; this.logging( `Cache complete initialization with DurationInMs:${elapsedTime}`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.trace, "Oav.liveValidator.initialize" ); this.logging( `Cache complete initialization`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.perfTrace, "Oav.liveValidator.initialize", elapsedTime ); if (this.options.loadValidatorInBackground) { this.logging("Building validator in background..."); // eslint-disable-next-line @typescript-eslint/no-floating-promises this.loadAllSpecValidatorInBackground(allSpecs); } } public isLoadInBackgroundCompleted() { return this.loadInBackgroundComplete; } private async loadAllSpecValidatorInBackground(allSpecs: SwaggerSpec[]) { const backgroundStartTime = Date.now(); utils.shuffleArray(allSpecs); while (allSpecs.length > 0) { let spec; try { spec = allSpecs.shift()!; const startTime = Date.now(); this.logging( `Start building validator for ${spec._filePath} in background`, LiveValidatorLoggingLevels.debug, LiveValidatorLoggingTypes.trace, "Oav.liveValidator.loadAllSpecValidatorInBackground" ); await this.loader!.buildAjvValidator(spec, { inBackground: true }); const elapsedTime = Date.now() - startTime; this.logging( `Complete building validator for ${spec._filePath} in background with DurationInMs:${elapsedTime}.`, LiveValidatorLoggingLevels.debug, LiveValidatorLoggingTypes.trace, "Oav.liveValidator.loadAllSpecValidatorInBackground" ); this.logging( `Complete building validator for ${spec._filePath} in background`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.perfTrace, "Oav.liveValidator.loadAllSpecValidatorInBackground", elapsedTime ); this.logging( `Complete building validator for spec ${spec._filePath}`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.specTrace, "Oav.liveValidator.loadAllSpecValidatorInBackground", elapsedTime, { providerNamespace: spec._providerNamespace ?? "unknown", apiVersion: spec.info.version, specName: spec._filePath, } ); } catch (e) { this.logging( `Failed to build validator for spec ${spec?._filePath}.\nErrorMessage:${ (e as any)?.message }.\nErrorStack:${(e as any)?.stack}`, LiveValidatorLoggingLevels.error, LiveValidatorLoggingTypes.specTrace, "Oav.liveValidator.loadAllSpecValidatorInBackground", undefined, { providerNamespace: spec?._providerNamespace ?? "unknown", apiVersion: spec?.info.version ?? "unknown", specName: spec?._filePath, } ); } } this.loader = undefined; this.loadInBackgroundComplete = true; const elapsedTimeForBuild = Date.now() - backgroundStartTime; this.logging( `Completed building validator for all specs in background with DurationInMs:${elapsedTimeForBuild}.`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.trace, "Oav.liveValidator.loadAllSpecValidatorInBackground" ); this.logging( `Completed building validator for all specs in background`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.perfTrace, "Oav.liveValidator.loadAllSpecValidatorInBackground", elapsedTimeForBuild ); } /** * Validates live request. */ public async validateLiveRequest( liveRequest: LiveRequest, options: ValidateOptions = {}, operationInfo?: OperationContext ): Promise<LiveValidationResult> { const startTime = Date.now(); const { info, error } = this.getOperationInfo(liveRequest, operationInfo); if (error !== undefined) { this.logging( `ErrorMessage:${error.message}.ErrorStack:${error.stack}`, LiveValidatorLoggingLevels.error, LiveValidatorLoggingTypes.error, "Oav.liveValidator.validateLiveRequest", undefined, info.validationRequest ); return { isSuccessful: undefined, errors: [], runtimeException: error, operationInfo: info, }; } if (!liveRequest.query) { liveRequest.query = kvPairsToObject( new URL(liveRequest.url, "https://management.azure.com").searchParams ); } let errors: LiveValidationIssue[] = []; let runtimeException; try { errors = await validateSwaggerLiveRequest( liveRequest, info, this.loader, options.includeErrors, this.options.isArmCall, this.logging ); } catch (reqValidationError) { const msg = `An error occurred while validating the live request for operation ` + `"${info.operationId}". The error is:\n ` + `${util.inspect(reqValidationError, { depth: null })}`; runtimeException = { code: C.ErrorCodes.RequestValidationError.name, message: msg }; this.logging( msg, LiveValidatorLoggingLevels.error, LiveValidatorLoggingTypes.error, "Oav.liveValidator.validateLiveRequest", undefined, info.validationRequest ); } const elapsedTime = Date.now() - startTime; this.logging( `Complete request validation`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.perfTrace, "Oav.liveValidator.validateLiveRequest", elapsedTime, info.validationRequest ); if (!options.includeOperationMatch) { delete info.operationMatch; delete info.validationRequest; } return { isSuccessful: runtimeException ? undefined : errors.length === 0, operationInfo: info, errors, runtimeException, }; } /** * Validates live response. */ public async validateLiveResponse( liveResponse: LiveResponse, specOperation: { url: string; method: string }, options: ValidateOptions = {}, operationInfo?: OperationContext ): Promise<LiveValidationResult> { const startTime = Date.now(); const { info, error } = this.getOperationInfo(specOperation, operationInfo); if (error !== undefined) { this.logging( `ErrorMessage:${error.message}.ErrorStack:${error.stack}`, LiveValidatorLoggingLevels.error, LiveValidatorLoggingTypes.error, "Oav.liveValidator.validateLiveResponse", undefined, info.validationRequest ); return { isSuccessful: undefined, errors: [], runtimeException: error, operationInfo: { apiVersion: C.unknownApiVersion, operationId: C.unknownOperationId }, }; } let errors: LiveValidationIssue[] = []; let runtimeException; this.transformResponseStatusCode(liveResponse); try { errors = await validateSwaggerLiveResponse( liveResponse, info, this.loader, options.includeErrors, this.options.isArmCall, this.logging ); } catch (resValidationError) { const msg = `An error occurred while validating the live response for operation ` + `"${info.operationId}". The error is:\n ` + `${util.inspect(resValidationError, { depth: null })}`; runtimeException = { code: C.ErrorCodes.RequestValidationError.name, message: msg }; this.logging( msg, LiveValidatorLoggingLevels.error, LiveValidatorLoggingTypes.error, "Oav.liveValidator.validateLiveResponse", undefined, info.validationRequest ); } const elapsedTime = Date.now() - startTime; this.logging( `Complete response validation`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.perfTrace, "Oav.liveValidator.validateLiveResponse", elapsedTime, info.validationRequest ); if (!options.includeOperationMatch) { delete info.operationMatch; delete info.validationRequest; } return { isSuccessful: runtimeException ? undefined : errors.length === 0, operationInfo: info, errors, runtimeException, }; } /** * Validates live request and response. */ public async validateLiveRequestResponse( requestResponseObj: RequestResponsePair, options?: ValidateOptions ): Promise<RequestResponseLiveValidationResult> { const validationResult = { requestValidationResult: { errors: [], operationInfo: { apiVersion: C.unknownApiVersion, operationId: C.unknownOperationId }, }, responseValidationResult: { errors: [], operationInfo: { apiVersion: C.unknownApiVersion, operationId: C.unknownOperationId }, }, }; if (!requestResponseObj) { const message = 'requestResponseObj cannot be null or undefined and must be of type "object".'; return { ...validationResult, runtimeException: { code: C.ErrorCodes.IncorrectInput.name, message, }, }; } const errors = this.validateRequestResponsePair!({}, requestResponseObj); if (errors.length > 0) { const error = errors[0]; const message = `Found errors "${error.message}" in the provided input in path ${error.jsonPathsInPayload[0]}:\n` + `${util.inspect(requestResponseObj, { depth: null })}.`; return { ...validationResult, runtimeException: { code: C.ErrorCodes.IncorrectInput.name, message, }, }; } const request = requestResponseObj.liveRequest; const response = requestResponseObj.liveResponse; this.transformResponseStatusCode(response); const requestValidationResult = await this.validateLiveRequest(request, { ...options, includeOperationMatch: true, }); const info = requestValidationResult.operationInfo; const responseValidationResult = requestValidationResult.isSuccessful === undefined && requestValidationResult.runtimeException === undefined ? requestValidationResult : await this.validateLiveResponse( response, request, { ...options, includeOperationMatch: false, }, info ); delete info.validationRequest; delete info.operationMatch; return { requestValidationResult, responseValidationResult, }; } private transformResponseStatusCode(liveResponse: LiveResponse) { // If status code is passed as a status code string (e.g. "OK") transform it to the status code // number (e.g. '200'). if ( !http.STATUS_CODES[liveResponse.statusCode] && utils.statusCodeStringToStatusCode[liveResponse.statusCode.toLowerCase()] ) { liveResponse.statusCode = utils.statusCodeStringToStatusCode[liveResponse.statusCode.toLowerCase()]; } } public getOperationInfo( request: { url: string; method: string; headers?: { [propertyName: string]: string } }, operationInfo?: OperationContext ): { info: OperationContext; error?: any; } { const info = operationInfo ?? { apiVersion: C.unknownApiVersion, operationId: C.unknownOperationId, }; try { if (info.validationRequest === undefined) { info.validationRequest = parseValidationRequest( request.url, request.method, request.headers ); } if (info.operationMatch === undefined) { const result = this.operationSearcher.search(info.validationRequest); info.apiVersion = result.apiVersion; info.operationMatch = result.operationMatch; } info.operationId = info.operationMatch.operation.operationId!; return { info }; } catch (error) { return { info, error }; } } public getResolvedOperationInfo( request: { url: string; method: string; headers?: { [propertyName: string]: string } }, operationInfo?: OperationContext ): { info: OperationContext; error?: any; } { const info = operationInfo ?? { apiVersion: C.unknownApiVersion, operationId: C.unknownOperationId, }; try { if (info.validationRequest === undefined) { info.validationRequest = parseValidationRequest( request.url, request.method, request.headers ); } if (info.operationMatch === undefined) { const result = this.operationSearcher.search(info.validationRequest); info.apiVersion = result.apiVersion; info.operationMatch = result.operationMatch; } info.operationId = info.operationMatch.operation.operationId!; return { info }; } catch (error) { return { info, error }; } } private async getMatchedPaths(jsonsPattern: string | string[]): Promise<string[]> { const startTime = Date.now(); let matchedPaths: string[] = []; if (typeof jsonsPattern === "string") { matchedPaths = glob.sync(jsonsPattern, { ignore: this.options.excludedSwaggerPathsPattern, nodir: true, }); } else { for (const pattern of jsonsPattern) { const res: string[] = glob.sync(pattern, { ignore: this.options.excludedSwaggerPathsPattern, nodir: true, }); for (const path of res) { if (!matchedPaths.includes(path)) { matchedPaths.push(path); } } } } this.logging( `Using swaggers found from directory: "${ this.options.directory }" and pattern: "${jsonsPattern.toString()}". Total paths count: ${matchedPaths.length}`, LiveValidatorLoggingLevels.info ); this.logging( `Using swaggers found from directory: "${ this.options.directory }" and pattern: "${jsonsPattern.toString()}". Total paths count: ${matchedPaths.length}`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.perfTrace, "Oav.livevalidator.getMatchedPaths", Date.now() - startTime ); return matchedPaths; } public async validateRoundTrip( requestResponseObj: RequestResponsePair ): Promise<LiveValidationResult> { const startTime = Date.now(); if (this.operationSearcher === undefined) { const msg = "operationSearcher should be initialized before this call."; const runtimeException = { code: C.ErrorCodes.RoundtripValidationError.name, message: msg }; return { isSuccessful: undefined, errors: [], operationInfo: { operationId: "", apiVersion: "", }, runtimeException: runtimeException, }; } const { info, error } = this.getResolvedOperationInfo(requestResponseObj.liveRequest); console.log(info.operationId); if (error !== undefined) { this.logging( `ErrorMessage:${error.message}.ErrorStack:${error.stack}`, LiveValidatorLoggingLevels.error, LiveValidatorLoggingTypes.error, "Oav.liveValidator.validateRoundTrip", undefined, info.validationRequest ); return { isSuccessful: undefined, errors: [], runtimeException: error, operationInfo: info, }; } let errors: LiveValidationIssue[] = []; let runtimeException; try { const res = diffRequestResponse( requestResponseObj, info, this.loader?.getResolvedJsonLoader()! ); for (const re of res) { if (re !== undefined) { errors.push(re); } } } catch (validationErr) { const msg = `An error occurred while validating the live request for operation ` + `"${info.operationId}". The error is:\n ` + `${util.inspect(validationErr, { depth: null })}`; runtimeException = { code: C.ErrorCodes.RoundtripValidationError.name, message: msg }; this.logging( msg, LiveValidatorLoggingLevels.error, LiveValidatorLoggingTypes.error, "Oav.liveValidator.validateRoundTrip", undefined, info.validationRequest ); return { isSuccessful: undefined, operationInfo: info, errors, runtimeException, }; } const elapsedTime = Date.now() - startTime; this.logging( `Complete roundtrip validation`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.perfTrace, "Oav.liveValidator.validateRoundTrip", elapsedTime, info.validationRequest ); delete info.validationRequest; delete info.operationMatch; return { isSuccessful: runtimeException ? undefined : errors.length === 0, operationInfo: info, errors, runtimeException, }; } private async getSwaggerPaths(): Promise<string[]> { if (this.options.swaggerPaths.length !== 0) { this.logging( `Using user provided swagger paths by options.swaggerPaths. Total paths count: ${this.options.swaggerPaths.length}` ); return this.options.swaggerPaths; } else { const allJsonsPattern = path.join(this.options.directory, "/specification/**/*.json"); const swaggerPathPatterns: string[] = []; if ( this.options.swaggerPathsPattern === undefined || this.options.swaggerPathsPattern.length === 0 ) { return this.getMatchedPaths(allJsonsPattern); } else { this.options.swaggerPathsPattern.map((item) => { swaggerPathPatterns.push(path.join(this.options.directory, item)); }); return this.getMatchedPaths(swaggerPathPatterns); } } } private async getSwaggerInitializer( loader: LiveValidatorLoader, swaggerPath: string ): Promise<SwaggerSpec | undefined> { const startTime = Date.now(); this.logging(`Building cache from:${swaggerPath}`, LiveValidatorLoggingLevels.debug); let spec; let resolvedSpec; try { spec = await loader.load(pathResolve(swaggerPath)); const elapsedTimeLoadSpec = Date.now() - startTime; this.logging( `Load spec ${swaggerPath}`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.perfTrace, "Oav.liveValidator.getSwaggerInitializer.loader.load", elapsedTimeLoadSpec ); const startTimeAddSpecToCache = Date.now(); if (this.options.enableRoundTripValidator) { resolvedSpec = await loader.load(pathResolve(swaggerPath), true); this.operationSearcher.addSpecToCache(resolvedSpec); } else { this.operationSearcher.addSpecToCache(spec); } // TODO: add data-plane RP to cache. this.logging( `Add spec to cache ${swaggerPath}`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.perfTrace, "Oav.liveValidator.getSwaggerInitializer.operationSearcher.addSpecToCache", Date.now() - startTimeAddSpecToCache ); const elapsedTime = Date.now() - startTime; this.logging( `Complete loading spec ${spec._filePath}`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.specTrace, "Oav.liveValidator.getSwaggerInitializer", elapsedTime, { providerNamespace: spec._providerNamespace ?? "unknown", apiVersion: spec.info.version, specName: spec._filePath, } ); return spec; } catch (err) { this.logging( `Unable to initialize "${swaggerPath}" file from SpecValidator. We are ` + `ignoring this swagger file and continuing to build cache for other valid specs.\nErrorMessage: ${ (err as any)?.message };\nErrorStack: ${(err as any)?.stack}`, LiveValidatorLoggingLevels.warn, LiveValidatorLoggingTypes.error ); const pathProvider = getProviderFromSpecPath(swaggerPath); this.logging( `Failed to load spec ${swaggerPath}`, LiveValidatorLoggingLevels.error, LiveValidatorLoggingTypes.specTrace, "Oav.liveValidator.getSwaggerInitializer", undefined, { providerNamespace: pathProvider ? pathProvider.provider : "unknown", apiVersion: spec ? spec.info.version : "unknown", specName: swaggerPath, } ); return undefined; } } private logging = ( message: string, level?: LiveValidatorLoggingLevels, loggingType?: LiveValidatorLoggingTypes, operationName?: string, durationInMilliseconds?: number, validationRequest?: ValidationRequest ) => { level = level || LiveValidatorLoggingLevels.info; loggingType = loggingType || LiveValidatorLoggingTypes.trace; operationName = operationName || ""; durationInMilliseconds = durationInMilliseconds || 0; if (this.logFunction !== undefined) { if (validationRequest !== undefined && validationRequest !== null) { this.logFunction(message, level, { CorrelationId: validationRequest.correlationId, ActivityId: validationRequest.activityId, ProviderNamespace: validationRequest.providerNamespace, ResourceType: validationRequest.resourceType, ApiVersion: validationRequest.apiVersion, OperationName: operationName, LoggingType: loggingType, DurationInMilliseconds: durationInMilliseconds, SpecName: validationRequest.specName, }); } else { this.logFunction(message, level, { OperationName: operationName, LoggingType: loggingType, DurationInMilliseconds: durationInMilliseconds, }); } } else { log.log(level, message); } }; } /** * OAV expects the url that is sent to match exactly with the swagger path. For this we need to keep only the part after * where the swagger path starts. Currently those are '/subscriptions' and '/providers'. */ export function formatUrlToExpectedFormat(requestUrl: string): string { return requestUrl.substring(requestUrl.search("/?(subscriptions|providers)/i")); } /** * Parse the validation request information. * * @param requestUrl The url of service api call. * * @param requestMethod The http verb for the method to be used for lookup. * * @param correlationId The id to correlate the api calls. * * @param activityId The id maps to request id, used by RPaaS. * * @returns parsed ValidationRequest info. * * @deprecated use parseValidationRequest instead. */ export const legacyParseValidationRequest = ( requestUrl: string, requestMethod: string | undefined | null, correlationId: string, activityId: string ): ValidationRequest => { if ( requestUrl === undefined || requestUrl === null || typeof requestUrl.valueOf() !== "string" || !requestUrl.trim().length ) { const msg = "An error occurred while trying to parse validation payload." + 'requestUrl is a required parameter of type "string" and it cannot be an empty string.'; const e = new models.LiveValidationError(C.ErrorCodes.PotentialOperationSearchError.name, msg); throw e; } if ( requestMethod === undefined || requestMethod === null || typeof requestMethod.valueOf() !== "string" || !requestMethod.trim().length ) { const msg = "An error occurred while trying to parse validation payload." + 'requestMethod is a required parameter of type "string" and it cannot be an empty string.'; const e = new models.LiveValidationError(C.ErrorCodes.PotentialOperationSearchError.name, msg); throw e; } return parseValidationRequest(requestUrl, requestMethod, { "x-ms-correlation-request-id": correlationId, "x-ms-request-id": activityId, }); }; /** * Parse the validation request information. * * @param requestUrl The url of service api call. * * @param requestMethod The http verb for the method to be used for lookup. * * @param headers Optional headers * * @returns parsed ValidationRequest info. */ export const parseValidationRequest = ( requestUrl: string, requestMethod: string, headers?: { [propertyName: string]: string } ): ValidationRequest => { let queryStr; let apiVersion = ""; let resourceType = ""; let providerNamespace = ""; const parsedUrl = new URL(requestUrl, "https://management.azure.com"); const pathStr = parsedUrl.pathname || ""; if (pathStr !== "") { // Lower all the keys and values of query parameters before searching for `api-version` const queryObject: ParsedUrlQuery = {}; parsedUrl.searchParams.forEach((value, key) => { queryObject[key.toLowerCase()] = value.toLowerCase(); }); apiVersion = (queryObject["api-version"] || C.unknownApiVersion) as string; providerNamespace = getProviderFromPathTemplate(pathStr) || C.unknownResourceProvider; resourceType = utils.getResourceType(pathStr, providerNamespace); // Provider would be provider found from the path or Microsoft.Unknown providerNamespace = providerNamespace || C.unknownResourceProvider; if (providerNamespace === C.unknownResourceProvider) { //apiVersion = C.unknownApiVersion; } providerNamespace = providerNamespace.toLowerCase(); apiVersion = apiVersion.toLowerCase(); queryStr = queryObject; requestMethod = requestMethod.toLowerCase(); } return { providerNamespace, resourceType, apiVersion, requestMethod: requestMethod as LowerHttpMethods, host: parsedUrl.host!, pathStr, query: queryStr, requestUrl: requestUrl, correlationId: headers?.["x-ms-correlation-request-id"] || "", activityId: headers?.["x-ms-request-id"] || "", }; };