UNPKG

powerplatform-review-tool

Version:

Evaluate Power Platform solution zip files based on best practice patterns

747 lines (746 loc) 34.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.patternCheckUnusedVariables = patternCheckUnusedVariables; exports.patternOptimizeActionCounts = patternOptimizeActionCounts; exports.patternCheckConsistentNaming = patternCheckConsistentNaming; exports.patternCheckNestedLoops = patternCheckNestedLoops; exports.patternCheckConcurrencySettings = patternCheckConcurrencySettings; exports.patternCheckTriggerCondition = patternCheckTriggerCondition; exports.patternCheckMultipleInitializeVariable = patternCheckMultipleInitializeVariable; exports.patternCheckStaticVariables = patternCheckStaticVariables; exports.patternCheckPowerAppResponse = patternCheckPowerAppResponse; exports.patternCheckScopeUsage = patternCheckScopeUsage; exports.patternCheckDataRetrievalParameters = patternCheckDataRetrievalParameters; exports.patternCheckForActionNote = patternCheckForActionNote; exports.patternCheckCreateUpdateInsideLoop = patternCheckCreateUpdateInsideLoop; const ManifestConstant_1 = require("../ManifestConstant"); const flowHelper_1 = require("../utilities/flowHelper"); // 2. Unused Variables Check // eslint-disable-next-line sonarjs/cognitive-complexity function patternCheckUnusedVariables(jsonContent) { const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.UnusedVariables; const declaredVariables = (0, flowHelper_1.getDeclaredVariables)(jsonContent); const usedVariables = (0, flowHelper_1.getVariablesUsedInVariableActions)(jsonContent); const unusedVariables = []; // Recursive helper function to search for variable references throughout the entire JSON function searchEntireJSON(obj, variableName) { if (typeof obj === "string" && obj.includes(`variables('${variableName}')`)) { // Return true if we find the variable reference at any point in the JSON return true; } if (obj && typeof obj === "object") { // Recursively search each value in objects and arrays return Object.values(obj).some((value) => searchEntireJSON(value, variableName)); } return false; } // Iterate over each declared variable and search for its usage in the entire JSON declaredVariables.forEach((variable) => { if (searchEntireJSON(jsonContent, variable.name)) { usedVariables.add(variable.name); } }); // Identify declared but unused variables declaredVariables.forEach((variable) => { if (!usedVariables.has(variable.name)) { unusedVariables.push(variable.name); } }); // console.log("Unused Variables:", unusedVariables); return { patternID, patternName, description, status: unusedVariables.length === 0 ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */, instanceValue: unusedVariables, failureReason: unusedVariables.length > 0 ? "Unused variables detected." : "", recommendation, docLinks: docLink, severity, category, }; } // 3. Action Count Optimization // eslint-disable-next-line sonarjs/cognitive-complexity function patternOptimizeActionCounts(jsonContent) { const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.ActionCount; // Recursive function to count all actions within every "actions" key in the JSON function countAllActions(obj) { let count = 0; if (obj && typeof obj === "object") { // Ensure it's an object // Check if "actions" is present and is an object if (obj.hasOwnProperty("actions") && typeof obj.actions === "object") { count += Object.keys(obj.actions).length; } // Recursively search through all nested objects and arrays Object.values(obj).forEach((nestedValue) => { if (typeof nestedValue === "object" || Array.isArray(nestedValue)) { count += countAllActions(nestedValue); } }); } else if (Array.isArray(obj)) { // Handle array of objects obj.forEach((item) => { count += countAllActions(item); }); } return count; } // Start counting actions from the root JSON content const actionCount = countAllActions(jsonContent); // console.log("Total action count:", actionCount); const exceedsLimit = actionCount > 50; return { patternID, patternName, description, status: !exceedsLimit ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */, instanceValue: exceedsLimit ? [`${actionCount} actions detected, consider reducing to 50 or less actions.`] : [], failureReason: exceedsLimit ? "Action count exceeds recommended limit of 50 actions per flow." : "", recommendation, docLinks: docLink, severity, category, }; } // 4. Consistent Naming for Flow Components function patternCheckConsistentNaming(jsonContent) { const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.NamingConventions; const actions = jsonContent?.properties?.definition?.actions || {}; const triggers = jsonContent?.properties?.definition?.triggers || {}; const inconsistentNames = []; function checkNaming(obj, prefix) { if (!obj.manual) { Object.keys(obj).forEach((key) => { if (!key.startsWith(prefix)) inconsistentNames.push(key); }); } } checkNaming(triggers, "Trg_"); checkNaming(actions, "Act_"); return { patternID, patternName, description, status: inconsistentNames.length === 0 ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Warning" /* ManifestPropertyStatusNames.Warning */, instanceValue: inconsistentNames, failureReason: inconsistentNames.length > 0 ? "Inconsistent naming detected." : "", recommendation, docLinks: docLink, severity, category, }; } // 5. Check nested loops // eslint-disable-next-line sonarjs/cognitive-complexity function patternCheckNestedLoops(jsonContent) { const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.NestedLoops; const actions = jsonContent?.properties?.definition?.actions || {}; const nestedLoops = []; const visitedActions = new Set(); // Track visited actions function checkLoops(action, actionName, parentLoopStack) { if (action && typeof action === "object") { // Check if we've already visited this action if (visitedActions.has(actionName)) { return; // Skip if already visited } visitedActions.add(actionName); const actionType = action.type; // If the action is a loop if (actionType === "Foreach" || actionType === "Until") { // If there's already a loop in the stack, this is a nested loop if (parentLoopStack.length > 0) { const parentLoopName = parentLoopStack[parentLoopStack.length - 1]; nestedLoops.push(`${parentLoopName} -> ${actionName}`); } // Add this loop to the stack parentLoopStack.push(actionName); } // Traverse actions within this action if (action.actions) { Object.keys(action.actions).forEach((nestedActionName) => { const nestedAction = action.actions[nestedActionName]; checkLoops(nestedAction, nestedActionName, [...parentLoopStack]); }); } // Handle conditional actions if (actionType === "If") { // 'Then' branch if (action.actions) { Object.keys(action.actions).forEach((nestedActionName) => { const nestedAction = action.actions[nestedActionName]; checkLoops(nestedAction, nestedActionName, [...parentLoopStack]); }); } // 'Else' branch if (action.else && action.else.actions) { Object.keys(action.else.actions).forEach((nestedActionName) => { const nestedAction = action.else.actions[nestedActionName]; checkLoops(nestedAction, nestedActionName, [...parentLoopStack]); }); } } // Handle switch cases if (actionType === "Switch" && action.cases) { Object.entries(action.cases).forEach(([, caseObj]) => { if (caseObj.actions) { Object.keys(caseObj.actions).forEach((nestedActionName) => { const nestedAction = caseObj.actions[nestedActionName]; checkLoops(nestedAction, nestedActionName, [...parentLoopStack]); }); } }); } // Handle scopes if (actionType === "Scope" && action.actions) { Object.keys(action.actions).forEach((nestedActionName) => { const nestedAction = action.actions[nestedActionName]; checkLoops(nestedAction, nestedActionName, [...parentLoopStack]); }); } // If we added this loop to the stack, remove it after processing if (actionType === "Foreach" || actionType === "Until") { parentLoopStack.pop(); } } } Object.keys(actions).forEach((actionName) => { const action = actions[actionName]; checkLoops(action, actionName, []); }); return { patternID, patternName, description, status: nestedLoops.length === 0 ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */, instanceValue: nestedLoops, failureReason: nestedLoops.length > 0 ? "Nested loops detected." : "", recommendation, docLinks: docLink, severity, category, }; } // 7. Parallel Execution and Concurrency Check // eslint-disable-next-line sonarjs/cognitive-complexity function patternCheckConcurrencySettings(jsonContent) { const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.Concurrency; const missingConcurrency = []; function traverseActions(actions) { if (actions && typeof actions === "object") { Object.keys(actions).forEach((actionName) => { const action = actions[actionName]; // Check if the action is of type 'Foreach' (loop) if (action.type === "Foreach" && !action.runtimeConfiguration?.concurrency?.repetitions) { missingConcurrency.push(actionName); } // Handle different action types if (action.type === "If") { // 'If' action: traverse 'then' and 'else' branches if (action.actions) { traverseActions(action.actions); } if (action.else && action.else.actions) { traverseActions(action.else.actions); } } else if (action.type === "Switch" && action.cases) { // 'Switch' action: traverse each case's actions Object.values(action.cases).forEach((caseObj) => { if (caseObj.actions) { traverseActions(caseObj.actions); } }); } else if (action.type === "Until" && action.actions) { // 'Until' loop: traverse its actions traverseActions(action.actions); } else if (action.type === "Scope" && action.actions) { // 'Scope' action: traverse its actions traverseActions(action.actions); } else if (action.actions) { // Any other action with 'actions' traverseActions(action.actions); } }); } } const actions = jsonContent?.properties?.definition?.actions || {}; traverseActions(actions); return { patternID, patternName, description, status: missingConcurrency.length === 0 ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */, instanceValue: missingConcurrency, failureReason: missingConcurrency.length > 0 ? "Concurrency settings missing." : "", recommendation, docLinks: docLink, severity, category, }; } // 8. Avoid Infinite Runs function patternCheckTriggerCondition(jsonContent) { const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.TriggerCondition; const triggers = jsonContent?.properties?.definition?.triggers || {}; const infiniteRunTriggers = []; Object.keys(triggers).forEach((triggerName) => { const trigger = triggers[triggerName]; if (!(trigger.type === "Recurrence" || trigger.type === "Request") && (!trigger.conditions || trigger.conditions.length === 0)) { infiniteRunTriggers.push(triggerName); } }); return { patternID, patternName, description, status: infiniteRunTriggers.length === 0 ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */, instanceValue: infiniteRunTriggers, failureReason: infiniteRunTriggers.length > 0 ? "Missing trigger conditions to avoid unnecessary flow executions." : "", recommendation, docLinks: docLink, severity, category, }; } // 9. Multiple InitializeVariable Actions Check // eslint-disable-next-line sonarjs/cognitive-complexity function patternCheckMultipleInitializeVariable(jsonContent) { const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.MultipleInitializeVariableActions; let initializeVariableCount = 0; function countInitializeVariableActions(obj) { if (obj && typeof obj === "object") { if (obj.type === "InitializeVariable") { initializeVariableCount += 1; } if (obj.actions) { for (const actionName in obj.actions) { countInitializeVariableActions(obj.actions[actionName]); } } } } countInitializeVariableActions(jsonContent.properties.definition); const hasMultipleInitializeVariable = initializeVariableCount > 1; return { patternID, patternName, description, status: !hasMultipleInitializeVariable ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */, instanceValue: hasMultipleInitializeVariable ? [`InitializeVariable actions count: ${initializeVariableCount}`] : [], failureReason: hasMultipleInitializeVariable ? "More than one 'InitializeVariable' action detected." : "", recommendation, docLinks: docLink, severity, category, }; } // 10. Check Static Variables // eslint-disable-next-line sonarjs/cognitive-complexity function patternCheckStaticVariables(jsonContent) { const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.StaticVariables; const initializedVariables = new Set(); const updatedVariables = new Set(); const staticVariables = []; function traverseActions(obj) { if (obj && typeof obj === "object") { // Check if the current object is an action if (obj.type) { if (obj.type === "InitializeVariable") { const variableName = obj.inputs?.variables?.[0]?.name; if (variableName) { initializedVariables.add(variableName); } } else if (obj.type === "SetVariable" || obj.type === "IncrementVariable" || obj.type === "DecrementVariable" || obj.type === "AppendToStringVariable" || obj.type === "AppendToArrayVariable") { const variableName = obj.inputs?.name; if (variableName) { updatedVariables.add(variableName); } } } // Recursively traverse nested actions if (obj.actions) { for (const actionName in obj.actions) { traverseActions(obj.actions[actionName]); } } // Traverse other properties for (const key in obj) { if (key !== "actions" && obj.hasOwnProperty(key)) { traverseActions(obj[key]); } } } } traverseActions(jsonContent.properties.definition); // Identify variables that are initialized but never updated initializedVariables.forEach((variableName) => { if (!updatedVariables.has(variableName)) { staticVariables.push(variableName); } }); const hasStaticVariables = staticVariables.length > 0; return { patternID, patternName, description, status: !hasStaticVariables ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */, instanceValue: staticVariables, failureReason: hasStaticVariables ? "Variables initialized but never updated detected. Use " : "", recommendation, docLinks: docLink, severity, category, }; } // 11. PowerApp Response Check // eslint-disable-next-line sonarjs/cognitive-complexity function patternCheckPowerAppResponse(jsonContent) { const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.PowerAppResponse; let powerAppResponseFound = false; function traverseActions(obj) { if (obj && typeof obj === "object") { // Check if the current object is a 'Response' action with kind 'PowerApp' or 'Http' and operationOptions property does not exist if (obj.type === "Response" && (obj.kind === "PowerApp" || obj.kind === "Http") && !obj.operationOptions) { powerAppResponseFound = true; } // Recursively traverse nested actions if (obj.actions) { for (const actionName in obj.actions) { traverseActions(obj.actions[actionName]); } } // Traverse other properties for (const key in obj) { if (key !== "actions" && obj.hasOwnProperty(key)) { traverseActions(obj[key]); } } } } traverseActions(jsonContent.properties.definition); return { patternID, patternName, description, status: !powerAppResponseFound ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */, instanceValue: powerAppResponseFound ? ["PowerApp response action found"] : [], failureReason: powerAppResponseFound ? "Warning: Logic may fail if the response is not sent within 120 seconds." : "", recommendation, docLinks: docLink, severity, category, }; } // 12. Scope Usage with Action Count Check // eslint-disable-next-line sonarjs/cognitive-complexity function patternCheckScopeUsage(jsonContent) { const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.ScopeUsage; // let actionCount = 0; let hasScopeAction = false; // Recursive function to count all actions within every "actions" key in the JSON function countAllActions(obj) { let count = 0; if (obj && typeof obj === "object") { // Ensure it's an object // Check if "actions" is present and is an object if (obj.hasOwnProperty("actions") && typeof obj.actions === "object") { count += Object.keys(obj.actions).length; } // Check if the current action is of type "Scope" if (obj.type === "Scope") { hasScopeAction = true; } // Recursively search through all nested objects and arrays Object.values(obj).forEach((nestedValue) => { if (typeof nestedValue === "object" || Array.isArray(nestedValue)) { count += countAllActions(nestedValue); } }); } else if (Array.isArray(obj)) { // Handle array of objects obj.forEach((item) => { count += countAllActions(item); }); } return count; } // Start counting actions from the root JSON content const actionCount = countAllActions(jsonContent); // console.log("Total action count:", actionCount); const hasManyActions = actionCount > 10; const lacksScope = !hasScopeAction; const patternFails = hasManyActions && lacksScope; return { patternID, patternName, description, status: !patternFails ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */, instanceValue: patternFails ? [`Action count: ${actionCount}`] : [], failureReason: patternFails ? "Use Scopes to group actions and implement error handling (with Run After conditions) to handle failures gracefully." : "", recommendation, docLinks: docLink, severity, category, }; } // 13. Data Retrieval Parameters Check // eslint-disable-next-line sonarjs/cognitive-complexity function patternCheckDataRetrievalParameters(jsonContent) { const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.DataRetrievalParameters; const missingParameters = []; // Helper function to format missing parameters function formatMissingParams(params) { if (params.length === 1) { return params[0]; } else if (params.length === 2) { return `${params[0]} and ${params[1]}`; } else { return `${params.slice(0, -1).join(", ")}, and ${params[params.length - 1]}`; } } const processedActions = new Set(); // Set to track processed actions function traverseActions(actions) { if (actions && typeof actions === "object") { Object.keys(actions).forEach((actionName) => { const action = actions[actionName]; // Check if this action has already been processed if (processedActions.has(actionName)) { return; // Skip processing this action again } processedActions.add(actionName); // Check if action is of type 'OpenApiConnection' or 'OpenApiConnectionWebhook' if (action.type === "OpenApiConnection") { const inputs = action.inputs; if (inputs && inputs.host && inputs.parameters) { const operationId = inputs.host.operationId; const apiId = inputs.host.apiId || ""; if (operationId === "GetItems" && apiId.includes("sharepoint")) { // SharePoint GetItems action const parameters = inputs.parameters; const hasFilter = parameters.hasOwnProperty("$filter"); const hasTop = parameters.hasOwnProperty("$top"); if (!hasFilter || !hasTop) { missingParameters.push(`Action '${actionName}' (SharePoint GetItems) is missing '${!hasFilter ? "$filter" : ""}${!hasFilter && !hasTop ? " and " : ""}${!hasTop ? "$top" : ""}'.`); } } else if (operationId === "GetItem" && apiId.includes("commondataservice")) { // CDS GetItem action const parameters = inputs.parameters; const hasSelect = parameters.hasOwnProperty("$select"); if (!hasSelect) { missingParameters.push(`Action '${actionName}' (CDS GetItem) is missing '$select'.`); } } else if (operationId === "ListRecords" && apiId.includes("commondataservice")) { // CDS ListRecords action const parameters = inputs.parameters; const missingParams = []; if (!parameters.hasOwnProperty("$select")) { missingParams.push("$select"); } if (!parameters.hasOwnProperty("$filter")) { missingParams.push("$filter"); } if (!parameters.hasOwnProperty("$top")) { missingParams.push("$top"); } if (missingParams.length > 0) { const missingParamsStr = formatMissingParams(missingParams); missingParameters.push(`Action '${actionName}' (CDS ListRecords) is missing '${missingParamsStr}'.`); } } } } // Recursively check nested actions if (action.actions) { traverseActions(action.actions); } // Check for actions within conditions (e.g., 'If' actions) if (action.type === "If") { if (action.actions) { traverseActions(action.actions); } if (action.else && action.else.actions) { traverseActions(action.else.actions); } } // Check for actions within switch cases if (action.type === "Switch" && action.cases) { Object.values(action.cases).forEach((caseObj) => { if (caseObj.actions) { traverseActions(caseObj.actions); } }); } // Check for actions within loops if ((action.type === "Foreach" || action.type === "Until") && action.actions) { traverseActions(action.actions); } }); } } const actions = jsonContent?.properties?.definition?.actions || {}; traverseActions(actions); const hasIssue = missingParameters.length > 0; return { patternID, patternName, description, status: !hasIssue ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */, instanceValue: missingParameters, failureReason: hasIssue ? "Missing recommended parameters in data retrieval actions." : "", recommendation, docLinks: docLink, severity, category, }; } // eslint-disable-next-line sonarjs/cognitive-complexity function patternCheckForActionNote(jsonContent) { const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.ActionNote; const actionsWithoutDescription = []; function traverseActions(actions) { if (actions && typeof actions === "object") { Object.keys(actions).forEach((actionName) => { const action = actions[actionName]; // Check if the action has a description if (!action.description || action.description.trim() === "") { actionsWithoutDescription.push(actionName); } // Recursively check nested actions if (action.actions) { traverseActions(action.actions); } // Handle conditional actions if (action.type === "If") { if (action.actions) traverseActions(action.actions); if (action.else?.actions) traverseActions(action.else.actions); } // Handle switch cases if (action.type === "Switch" && action.cases) { Object.values(action.cases).forEach((caseObj) => { if (caseObj.actions) traverseActions(caseObj.actions); }); } // Handle loops if ((action.type === "Foreach" || action.type === "Until") && action.actions) { traverseActions(action.actions); } }); } } const actions = jsonContent?.properties?.definition?.actions || {}; traverseActions(actions); const hasIssue = actionsWithoutDescription.length > 0; return { patternID, patternName, description, status: !hasIssue ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Warning" /* ManifestPropertyStatusNames.Warning */, instanceValue: actionsWithoutDescription, failureReason: hasIssue ? "Actions missing notes." : "", recommendation, docLinks: docLink, severity, category, }; } // eslint-disable-next-line sonarjs/cognitive-complexity function patternCheckCreateUpdateInsideLoop(jsonContent) { const { patternID, patternName, description, docLink, recommendation, severity, category } = ManifestConstant_1.FlowPatternDetails.CreateUpdateInsideLoop; const createUpdateActions = new Set(); // Dataverse operation IDs const dataverseOperationIds = [ "CreateRecord", "CreateRecordWithOrganization", "UpdateOnlyRecord", "UpdateOnlyRecordWithOrganization", "UpdateRecord", "UpdateRecordWithOrganization", "DeleteRecord", "DeleteRecordWithOrganization", ]; // SharePoint operation IDs (sample IDs included) const sharePointOperationIds = ["CreateFile", "PostItem", "DeleteFile", "DeleteItem", "UpdateFile", "PatchItem"]; function traverseActions(actions, isInsideLoop) { if (actions && typeof actions === "object") { Object.keys(actions).forEach((actionName) => { const action = actions[actionName]; // Check for Create/Update in Dataverse or SharePoint within a loop const operationId = action.inputs?.host?.operationId; const apiId = action.inputs?.host?.apiId; if (isInsideLoop && action.type === "OpenApiConnection" && ((apiId?.includes("commondataservice") && dataverseOperationIds.includes(operationId)) || (apiId?.includes("sharepoint") && sharePointOperationIds.includes(operationId)))) { createUpdateActions.add(actionName); } // If action is a loop, traverse its nested actions if (action.type === "Foreach" || action.type === "Until") { traverseActions(action.actions, true); } // Recursively check nested actions if (action.actions) { traverseActions(action.actions, isInsideLoop); } // Handle conditional actions if (action.type === "If") { if (action.actions) { traverseActions(action.actions, isInsideLoop); } if (action.else?.actions) { traverseActions(action.else.actions, isInsideLoop); } } // Handle switch cases if (action.type === "Switch" && action.cases) { Object.values(action.cases).forEach((caseObj) => { if (caseObj.actions) { traverseActions(caseObj.actions, isInsideLoop); } }); } }); } } const actions = jsonContent?.properties?.definition?.actions || {}; traverseActions(actions, false); const hasIssue = createUpdateActions.size > 0; return { patternID, patternName, description, status: !hasIssue ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */, instanceValue: Array.from(createUpdateActions), failureReason: hasIssue ? "Create/Update actions found inside loops." : "", recommendation, docLinks: docLink, severity, category, }; }