UNPKG

sfdx-hardis

Version:

Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards

342 lines (339 loc) 16.8 kB
/* jscpd:ignore-start */ import { SfCommand, Flags, requiredOrgFlagWithDeprecations } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core'; import c from 'chalk'; import fs from 'fs-extra'; import * as path from 'path'; import sortArray from 'sort-array'; import { createTempDir, uxLog } from '../../../../common/utils/index.js'; import * as dns from 'dns'; import Papa from 'papaparse'; import { getNotificationButtons, getOrgMarkdown, getSeverityIcon } from '../../../../common/utils/notifUtils.js'; import { soqlQuery } from '../../../../common/utils/apiUtils.js'; import { WebSocketClient } from '../../../../common/websocketClient.js'; import { NotifProvider } from '../../../../common/notifProvider/index.js'; import { generateCsvFile, generateReportPath } from '../../../../common/utils/filesUtils.js'; import { CONSTANTS } from '../../../../config/index.js'; import { FileDownloader } from '../../../../common/utils/fileDownloader.js'; import { setConnectionVariables } from '../../../../common/utils/orgUtils.js'; const dnsPromises = dns.promises; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); export default class LegacyApi extends SfCommand { static title = 'Check for legacy API use'; static description = `Checks if an org uses retired or someday retired API version\n See article below [![Handle Salesforce API versions Deprecation like a pro](https://github.com/hardisgroupcom/sfdx-hardis/raw/main/docs/assets/images/article-deprecated-api.jpg)](https://nicolas.vuillamy.fr/handle-salesforce-api-versions-deprecation-like-a-pro-335065f52238) This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/salesforce-monitoring-deprecated-api-calls/) and can output Grafana, Slack and MsTeams Notifications. `; static examples = [ '$ sf hardis:org:diagnose:legacyapi', '$ sf hardis:org:diagnose:legacyapi -u hardis@myclient.com', "$ sf hardis:org:diagnose:legacyapi --outputfile 'c:/path/to/folder/legacyapi.csv'", '$ sf hardis:org:diagnose:legacyapi -u hardis@myclient.com --outputfile ./tmp/legacyapi.csv', ]; // public static args = [{name: 'file'}]; static flags = { eventtype: Flags.string({ char: 'e', default: 'ApiTotalUsage', description: 'Type of EventLogFile event to analyze', }), limit: Flags.integer({ char: 'l', default: 999, description: 'Number of latest EventLogFile events to analyze', }), outputfile: Flags.string({ char: 'f', description: 'Force the path and name of output report file. Must end with .csv', }), debug: Flags.boolean({ char: 'd', default: false, description: messages.getMessage('debugMode'), }), websocket: Flags.string({ description: messages.getMessage('websocket'), }), skipauth: Flags.boolean({ description: 'Skip authentication check when a default username is required', }), 'target-org': requiredOrgFlagWithDeprecations, }; static requiresProject = false; debugMode = false; apexSCannerCodeUrl = 'https://raw.githubusercontent.com/pozil/legacy-api-scanner/main/legacy-api-scanner.apex'; legacyApiDescriptors = [ { apiFamily: ['SOAP', 'REST', 'BULK_API'], minApiVersion: 1.0, maxApiVersion: 6.0, severity: 'ERROR', deprecationRelease: 'Summer 21 - retirement of 1 to 6', errors: [], }, { apiFamily: ['SOAP', 'REST', 'BULK_API'], minApiVersion: 7.0, maxApiVersion: 20.0, severity: 'ERROR', deprecationRelease: 'Summer 22 - retirement of 7 to 20', errors: [], }, { apiFamily: ['SOAP', 'REST', 'BULK_API'], minApiVersion: 21.0, maxApiVersion: 30.0, severity: 'WARNING', deprecationRelease: 'Summer 25 - retirement of 21 to 30', errors: [], }, ]; allErrors = []; ipResultsSorted = []; outputFile; outputFilesRes = {}; /* jscpd:ignore-end */ tempDir; async run() { const { flags } = await this.parse(LegacyApi); this.debugMode = flags.debug || false; return await this.runJsForce(flags); } // Refactoring of Philippe Ozil's apex script with JsForce queries async runJsForce(flags) { const eventType = flags.eventtype || 'ApiTotalUsage'; const limit = flags.limit || 999; this.outputFile = flags.outputfile || null; const limitConstraint = limit ? ` LIMIT ${limit}` : ''; const conn = flags['target-org'].getConnection(); this.tempDir = await createTempDir(); // Get EventLogFile records with EventType = 'ApiTotalUsage' const logCountQuery = `SELECT COUNT() FROM EventLogFile WHERE EventType = '${eventType}'`; const logCountRes = await soqlQuery(logCountQuery, conn); if (logCountRes.totalSize === 0) { uxLog(this, c.green(`Found no EventLogFile entry of type ${eventType}.`)); uxLog(this, c.green('This indicates that no legacy APIs were called during the log retention window.')); } else { uxLog(this, c.grey('Found ' + c.bold(logCountRes.totalSize) + ` ${eventType} EventLogFile entries.`)); } if (logCountRes.totalSize > limit) { uxLog(this, c.yellow(`There are more than ${limit} results, you may consider to increase limit using --limit argument`)); } // Fetch EventLogFiles with ApiTotalUsage entries const logCollectQuery = `SELECT LogFile FROM EventLogFile WHERE EventType = '${eventType}' ORDER BY LogDate DESC` + limitConstraint; uxLog(this, c.grey('Query: ' + c.italic(logCollectQuery))); const eventLogRes = await soqlQuery(logCollectQuery, conn); // Collect legacy api calls from logs uxLog(this, c.grey('Calling org API to get CSV content of each EventLogFile record, then parse and analyze it...')); for (const eventLogFile of eventLogRes.records) { await this.collectDeprecatedApiCalls(eventLogFile.LogFile, conn); } this.allErrors = [ ...this.legacyApiDescriptors[0].errors, ...this.legacyApiDescriptors[1].errors, ...this.legacyApiDescriptors[2].errors, ]; // Display summary uxLog(this, ''); uxLog(this, c.cyan('Results:')); for (const descriptor of this.legacyApiDescriptors) { const colorMethod = descriptor.severity === 'ERROR' && descriptor.errors.length > 0 ? c.red : descriptor.severity === 'WARNING' && descriptor.errors.length > 0 ? c.yellow : c.green; uxLog(this, colorMethod(`- ${descriptor.deprecationRelease} : ${c.bold(descriptor.errors.length)}`)); } uxLog(this, ''); // Build command result let msg = 'No deprecated API call has been found in ApiTotalUsage logs'; let statusCode = 0; if (this.legacyApiDescriptors.filter((descriptor) => descriptor.severity === 'ERROR' && descriptor.errors.length > 0) .length > 0) { msg = 'Found legacy API versions calls in logs'; statusCode = 1; uxLog(this, c.red(c.bold(msg))); } else if (this.legacyApiDescriptors.filter((descriptor) => descriptor.severity === 'WARNING' && descriptor.errors.length > 0).length > 0) { msg = 'Found deprecated API versions calls in logs that will not be supported anymore in the future'; statusCode = 0; uxLog(this, c.yellow(c.bold(msg))); } else { uxLog(this, c.green(msg)); } // Generate main CSV file this.outputFile = await generateReportPath('legacy-api-calls', this.outputFile); this.outputFilesRes = await generateCsvFile(this.allErrors, this.outputFile); // Generate one summary file by severity const outputFileIps = []; for (const descriptor of this.legacyApiDescriptors) { const errors = descriptor.errors; if (errors.length > 0) { const outputFileIp = await this.generateSummaryLog(errors, descriptor.severity); outputFileIps.push(outputFileIp); // Trigger command to open CSV file in VsCode extension WebSocketClient.requestOpenFile(outputFileIp); } } // Debug or manage CSV file generation error if (this.debugMode || this.outputFile == null) { for (const descriptor of this.legacyApiDescriptors) { uxLog(this, c.grey(`- ${descriptor.deprecationRelease} : ${JSON.stringify(descriptor.errors.length)}`)); } } let notifDetailText = ''; for (const descriptor of this.legacyApiDescriptors) { if (descriptor.errors.length > 0) { notifDetailText += `• ${descriptor.severity}: API version calls found in logs: ${descriptor.errors.length} (${descriptor.deprecationRelease})\n`; } } notifDetailText += ` See article to solve issue before it's too late: • EN: https://nicolas.vuillamy.fr/handle-salesforce-api-versions-deprecation-like-a-pro-335065f52238 • FR: https://leblog.hardis-group.com/portfolio/versions-dapi-salesforce-decommissionnees-que-faire/`; // Build notifications const orgMarkdown = await getOrgMarkdown(flags['target-org']?.getConnection()?.instanceUrl); const notifButtons = await getNotificationButtons(); let notifSeverity = 'log'; let notifText = `No deprecated Salesforce API versions are used in ${orgMarkdown}`; if (this.allErrors.length > 0) { notifSeverity = 'error'; notifText = `${this.allErrors.length} deprecated Salesforce API versions are used in ${orgMarkdown}`; } // Post notifications await setConnectionVariables(flags['target-org']?.getConnection()); // Required for some notifications providers like Email await NotifProvider.postNotifications({ type: 'LEGACY_API', text: notifText, attachments: [{ text: notifDetailText }], buttons: notifButtons, severity: notifSeverity, attachedFiles: this.outputFilesRes.xlsxFile ? [this.outputFilesRes.xlsxFile, this.outputFilesRes.xlsxFile2] : [], logElements: this.allErrors, data: { metric: this.allErrors.length, legacyApiSummary: this.ipResultsSorted, }, metrics: { LegacyApiCalls: this.allErrors.length, }, }); if ((this.argv || []).includes('legacyapi')) { process.exitCode = statusCode; } // Return an object to be displayed with --json return { status: statusCode, message: msg, csvLogFile: this.outputFile, outputFileIps: outputFileIps, legacyApiResults: this.legacyApiDescriptors, }; } // GET csv log file and check for legacy API calls within async collectDeprecatedApiCalls(logFileUrl, conn) { // Load icons const severityIconError = getSeverityIcon('error'); const severityIconWarning = getSeverityIcon('warning'); const severityIconInfo = getSeverityIcon('info'); // Download file as stream, and process chuck by chuck uxLog(this, c.grey(`- processing ${logFileUrl}...`)); const fetchUrl = `${conn.instanceUrl}${logFileUrl}`; const outputFile = path.join(this.tempDir, Math.random().toString(36).substring(7) + ".csv"); const downloadResult = await new FileDownloader(fetchUrl, { conn: conn, outputFile: outputFile }).download(); if (downloadResult.success) { uxLog(this, c.grey(`-- parsing downloaded CSV from ${outputFile} and check for deprecated calls...`)); const outputFileStream = fs.createReadStream(outputFile, { encoding: 'utf8' }); await new Promise((resolve, reject) => { Papa.parse(outputFileStream, { header: true, worker: true, chunk: (results) => { // Look in check the entries that match a deprecation description for (const logEntry of results.data) { const apiVersion = logEntry.API_VERSION ? parseFloat(logEntry.API_VERSION) : parseFloat('999.0'); const apiFamily = logEntry.API_FAMILY || null; for (const legacyApiDescriptor of this.legacyApiDescriptors) { if (legacyApiDescriptor.apiFamily.includes(apiFamily) && legacyApiDescriptor.minApiVersion <= apiVersion && legacyApiDescriptor.maxApiVersion >= apiVersion) { logEntry.SFDX_HARDIS_DEPRECATION_RELEASE = legacyApiDescriptor.deprecationRelease; logEntry.SFDX_HARDIS_SEVERITY = legacyApiDescriptor.severity; if (legacyApiDescriptor.severity === 'ERROR') { logEntry.severity = 'error'; logEntry.severityIcon = severityIconError; } else if (legacyApiDescriptor.severity === 'WARNING') { logEntry.severity = 'warning'; logEntry.severityIcon = severityIconWarning; } else { // severity === 'INFO' logEntry.severity = 'info'; logEntry.severityIcon = severityIconInfo; } legacyApiDescriptor.errors.push(logEntry); break; } } } }, complete: function () { resolve(true); }, error: function (error) { reject(error); }, }); }); } else { uxLog(this, c.yellow(`Warning: Unable to process logs of ${logFileUrl}`)); } } async generateSummaryLog(errors, severity) { // Collect all ips and the number of calls const ipList = {}; for (const eventLogRecord of errors) { if (eventLogRecord.CLIENT_IP) { const ipInfo = ipList[eventLogRecord.CLIENT_IP] || { count: 0 }; ipInfo.count++; ipList[eventLogRecord.CLIENT_IP] = ipInfo; } } // Try to get hostname for ips const ipResults = []; for (const ip of Object.keys(ipList)) { const ipInfo = ipList[ip]; let hostname; try { hostname = await dnsPromises.reverse(ip); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { hostname = 'unknown'; } const ipResult = { CLIENT_IP: ip, CLIENT_HOSTNAME: hostname, SFDX_HARDIS_COUNT: ipInfo.count }; ipResults.push(ipResult); } this.ipResultsSorted = sortArray(ipResults, { by: ['SFDX_HARDIS_COUNT'], order: ['desc'], }); // Write output CSV with client api info const outputFileIps = this.outputFile.endsWith('.csv') ? this.outputFile.replace('.csv', '.api-clients-' + severity + '.csv') : this.outputFile + 'api-clients-' + severity + '.csv'; const outputFileIpsRes = await generateCsvFile(this.ipResultsSorted, outputFileIps); if (outputFileIpsRes.xlsxFile) { this.outputFilesRes.xlsxFile2 = outputFileIpsRes.xlsxFile; } uxLog(this, c.italic(c.cyan(`Please see info about ${severity} API callers in ${c.bold(outputFileIps)}`))); return outputFileIps; } } //# sourceMappingURL=legacyapi.js.map