diff --git a/rules/001_project_settings/001_0009_strict_mode_with_optimized_client.js b/rules/001_project_settings/001_0009_strict_mode_with_optimized_client.js new file mode 100644 index 0000000..d2e1fd9 --- /dev/null +++ b/rules/001_project_settings/001_0009_strict_mode_with_optimized_client.js @@ -0,0 +1,42 @@ +const metadata = { + scope: "package", + title: "Ensure strict mode is enabled when using the React client", + description: "When React is enabled in Project Settings, StrictMode must also be enabled in Project Security for security and stability", + authors: ["Jurre Tanja "], + custom: { + category: "Security", + rulename: "StrictModeWithOptimizedClient", + severity: "HIGH", + rulenumber: "001_0009", + remediation: "Enable Strict mode in the Security settings", + input: ".*\\$ProjectSettings\\.yaml" + } +}; + +function rule(input = {}) { + const errors = []; + + try { + // Check if UseOptimizedClient is enabled + if (input.UseOptimizedClient === true) { + // Read the ProjectSecurity.yaml file to check StrictMode + try { + const securityContent = mxlint.io.readfile("Security$ProjectSecurity.yaml"); + + // Check if StrictMode is enabled + if (!securityContent || securityContent.StrictMode !== true) { + errors.push(`[${metadata.custom.severity}, ${metadata.custom.category}, ${metadata.custom.rulenumber}] StrictMode must be enabled in Project Security when UseOptimizedClient is enabled in Project Settings`); + } + } catch (securityError) { + errors.push(`[${metadata.custom.severity}, ${metadata.custom.category}, ${metadata.custom.rulenumber}] Failed to read Security$ProjectSecurity.yaml: ${securityError.message}`); + } + } + } catch (e) { + errors.push(`[${metadata.custom.severity}, ${metadata.custom.category}, ${metadata.custom.rulenumber}] Error checking strict mode configuration: ${e.message}`); + } + + return { + allow: errors.length === 0, + errors + }; +} diff --git a/rules/001_project_settings/001_0009_strict_mode_with_optimized_client_test.yaml b/rules/001_project_settings/001_0009_strict_mode_with_optimized_client_test.yaml new file mode 100644 index 0000000..3219eb3 --- /dev/null +++ b/rules/001_project_settings/001_0009_strict_mode_with_optimized_client_test.yaml @@ -0,0 +1,28 @@ +TestCases: + - name: allow when UseOptimizedClient is false + allow: true + input: + UseOptimizedClient: false + files: + "Security$ProjectSecurity.yaml": + StrictMode: false + - name: allow when both UseOptimizedClient and StrictMode are true + allow: true + input: + UseOptimizedClient: true + files: + "Security$ProjectSecurity.yaml": + StrictMode: true + - name: do not allow UseOptimizedClient true but StrictMode false + allow: false + input: + UseOptimizedClient: true + files: + "Security$ProjectSecurity.yaml": + StrictMode: false + - name: do not allow UseOptimizedClient true but StrictMode missing + allow: false + input: + UseOptimizedClient: true + files: + "Security$ProjectSecurity.yaml": {} diff --git a/rules/005_microflows/005_0006_commit_actions_with_a_loop_and_submicroflows.js b/rules/005_microflows/005_0006_commit_actions_with_a_loop_and_submicroflows.js new file mode 100644 index 0000000..c0997bf --- /dev/null +++ b/rules/005_microflows/005_0006_commit_actions_with_a_loop_and_submicroflows.js @@ -0,0 +1,188 @@ +const metadata = { + scope: "package", + title: "Commit actions with a loop (including submicroflows)", + description: "Commiting objects within a loop will fire a SQL Update query for each iteration. This includes commits in called submicroflows.", + authors: ["Viktor Berlov "], + custom: { + category: "Microflows", + rulename: "AvoidCommitInLoopWithSubmicroflows", + severity: "MEDIUM", + rulenumber: "005_0006", + remediation: "Consider committing objects outside the loop. Within the loop, add them to a list.", + input: ".*\\$Microflow\\.yaml" + } +}; + +function rule(input = {}) { + const errors = []; + const visitedMicroflows = new Set(); + + try { + // Check for commits directly in loops + checkCommitsInLoops(input, errors, ""); + + // Check for commits in called submicroflows within loops + checkSubmicroflowsInLoops(input, errors, visitedMicroflows, ""); + } catch (e) { + errors.push(`[${metadata.custom.severity}, ${metadata.custom.category}, ${metadata.custom.rulenumber}] Error checking commit actions in loop: ${e.message}`); + } + + return { + allow: errors.length === 0, + errors + }; +} + +/** + * Check for direct commit actions within loops + */ +function checkCommitsInLoops(microflow, errors, chain) { + const name = microflow.Name || "Unknown"; + const mainFunction = microflow.MainFunction || []; + + // Check for LoopedActivity with commit actions + mainFunction.forEach(attr => { + if (attr.Attributes && attr.Attributes["$Type"] === "Microflows$LoopedActivity") { + const objects = attr.Attributes.ObjectCollection?.Objects || []; + + objects.forEach(obj => { + // Check for direct CommitAction + if (obj.Action && obj.Action["$Type"] === "Microflows$CommitAction") { + const errorChain = chain ? `${chain} → ${name}` : name; + errors.push( + `[${metadata.custom.severity}, ${metadata.custom.category}, ${metadata.custom.rulenumber}] Commit actions inside ${errorChain} loop` + ); + } + + // Check for ChangeAction with Commit set to Yes + if (obj.Action && obj.Action["$Type"] === "Microflows$ChangeAction" && obj.Action.Commit === "Yes") { + const errorChain = chain ? `${chain} → ${name}` : name; + errors.push( + `[${metadata.custom.severity}, ${metadata.custom.category}, ${metadata.custom.rulenumber}] Commit set to Yes for Change actions inside ${errorChain} loop` + ); + } + }); + } + }); +} + +/** + * Recursively check for submicroflow calls within loops and their contents + */ +function checkSubmicroflowsInLoops(microflow, errors, visitedMicroflows, chain) { + const name = microflow.Name || "Unknown"; + + // Prevent infinite loops + if (visitedMicroflows.has(name)) { + return; + } + visitedMicroflows.add(name); + + const mainFunction = microflow.MainFunction || []; + + mainFunction.forEach(attr => { + if (attr.Attributes && attr.Attributes["$Type"] === "Microflows$LoopedActivity") { + const objects = attr.Attributes.ObjectCollection?.Objects || []; + + objects.forEach(obj => { + // Check for MicroflowCallAction (submicroflow calls) + if (obj.Action && obj.Action["$Type"] === "Microflows$MicroflowCallAction") { + const submicroflowName = extractMicroflowName(obj.Action); + + if (submicroflowName) { + try { + // Build the chain for error messages + const newChain = chain ? `${chain} → ${name}` : name; + + // Try to read the submicroflow + const submicroflowContent = mxlint.io.readfile(`${submicroflowName}$Microflow.yaml`); + + if (submicroflowContent) { + // Check if the submicroflow contains ANY commits (anywhere, not just in loops) + checkForAnyCommits(submicroflowContent, errors, newChain); + } + } catch (e) { + // Silently continue if submicroflow cannot be read + // This is normal as not all microflows may be available + } + } + } + }); + } + }); +} + +/** + * Recursively check if a microflow or any of its called submicroflows contains any commits + */ +function checkForAnyCommits(microflow, errors, chain) { + const name = microflow.Name || "Unknown"; + const mainFunction = microflow.MainFunction || []; + + mainFunction.forEach(attr => { + if (attr.Attributes) { + const objects = attr.Attributes.ObjectCollection?.Objects || []; + + objects.forEach(obj => { + // Check for any CommitAction + if (obj.Action && obj.Action["$Type"] === "Microflows$CommitAction") { + const errorChain = chain ? `${chain} → ${name}` : name; + errors.push( + `[${metadata.custom.severity}, ${metadata.custom.category}, ${metadata.custom.rulenumber}] Commit actions found in submicroflow called from loop: ${errorChain}` + ); + } + + // Check for ChangeAction with Commit set to Yes + if (obj.Action && obj.Action["$Type"] === "Microflows$ChangeAction" && obj.Action.Commit === "Yes") { + const errorChain = chain ? `${chain} → ${name}` : name; + errors.push( + `[${metadata.custom.severity}, ${metadata.custom.category}, ${metadata.custom.rulenumber}] Commit set to Yes in submicroflow called from loop: ${errorChain}` + ); + } + + // Recursively check nested submicroflow calls + if (obj.Action && obj.Action["$Type"] === "Microflows$MicroflowCallAction") { + const submicroflowName = extractMicroflowName(obj.Action); + + if (submicroflowName) { + try { + const newChain = chain ? `${chain} → ${name}` : name; + const submicroflowContent = mxlint.io.readfile(`${submicroflowName}$Microflow.yaml`); + + if (submicroflowContent) { + checkForAnyCommits(submicroflowContent, errors, newChain); + } + } catch (e) { + // Silently continue if submicroflow cannot be read + } + } + } + }); + } + }); +} + +/** + * Extract microflow name from MicroflowCallAction + * The microflow reference is typically in MicroflowName or microflowname property + */ +function extractMicroflowName(action) { + if (!action) return null; + + // Try various possible property names + const possibleNames = [ + action.MicroflowName, + action.microflowname, + action.MicroflowQualifiedName, + action.microflowQualifiedName + ]; + + for (const name of possibleNames) { + if (name && typeof name === "string") { + // If it's a qualified name like "Module.Microflow", keep as is + return name; + } + } + + return null; +} diff --git a/rules/005_microflows/005_0006_commit_actions_with_a_loop_and_submicroflows_test.yaml b/rules/005_microflows/005_0006_commit_actions_with_a_loop_and_submicroflows_test.yaml new file mode 100644 index 0000000..8f41df4 --- /dev/null +++ b/rules/005_microflows/005_0006_commit_actions_with_a_loop_and_submicroflows_test.yaml @@ -0,0 +1,250 @@ +TestCases: +- name: allow loop without objects + allow: true + input: + $Type: Microflow$Microflow + Name: MicroflowForLoop + MainFunction: + - Attributes: + $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: null + +- name: allow loop with change action without commit + allow: true + input: + $Type: Microflow$Microflow + Name: MicroflowForLoop + MainFunction: + - Attributes: + $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$ChangeAction + Commit: "No" + +- name: do not allow loop with commit action + allow: false + input: + $Type: Microflow$Microflow + Name: MicroflowForLoop + MainFunction: + - Attributes: + $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$CommitAction + +- name: do not allow loop with change action with commit + allow: false + input: + $Type: Microflow$Microflow + Name: MicroflowForLoop + MainFunction: + - Attributes: + $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$ChangeAction + Commit: "Yes" + +- name: do not allow loop with both commit and change actions + allow: false + input: + $Type: Microflow$Microflow + Name: MicroflowForLoop + MainFunction: + - Attributes: + $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$CommitAction + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$ChangeAction + Commit: "Yes" + +- name: allow loop with submicroflow call that has no commits + allow: true + input: + $Type: Microflow$Microflow + Name: MicroflowWithSubmicroflowCall + MainFunction: + - Attributes: + $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$MicroflowCallAction + MicroflowName: HelperModule.SafeSubmicroflow + files: + "HelperModule.SafeSubmicroflow$Microflow.yaml": + $Type: Microflow$Microflow + Name: SafeSubmicroflow + MainFunction: + - Attributes: + $Type: Microflows$ActionActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$LogMessage + +- name: do not allow loop with submicroflow call that contains commit + allow: false + input: + $Type: Microflow$Microflow + Name: MicroflowWithSubmicroflowCall + MainFunction: + - Attributes: + $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$MicroflowCallAction + MicroflowName: HelperModule.CommitSubmicroflow + files: + "HelperModule.CommitSubmicroflow$Microflow.yaml": + $Type: Microflow$Microflow + Name: CommitSubmicroflow + MainFunction: + - Attributes: + $Type: Microflows$ActionActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$CommitAction + +- name: do not allow nested submicroflow calls with commit deep in chain + allow: false + input: + $Type: Microflow$Microflow + Name: MainMicroflow + MainFunction: + - Attributes: + $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$MicroflowCallAction + MicroflowName: Module1.SubmicroflowA + files: + "Module1.SubmicroflowA$Microflow.yaml": + $Type: Microflow$Microflow + Name: SubmicroflowA + MainFunction: + - Attributes: + $Type: Microflows$ActionActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$MicroflowCallAction + MicroflowName: Module2.SubmicroflowB + "Module2.SubmicroflowB$Microflow.yaml": + $Type: Microflow$Microflow + Name: SubmicroflowB + MainFunction: + - Attributes: + $Type: Microflows$ActionActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$ChangeAction + Commit: "Yes" + +- name: do not allow submicroflow with commit outside of loop + allow: false + input: + $Type: Microflow$Microflow + Name: MicroflowWithSubmicroflowCall + MainFunction: + - Attributes: + $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$MicroflowCallAction + MicroflowName: Module.SubmicroflowWithCommitOutsideLoop + files: + "Module.SubmicroflowWithCommitOutsideLoop$Microflow.yaml": + $Type: Microflow$Microflow + Name: SubmicroflowWithCommitOutsideLoop + MainFunction: + - Attributes: + $Type: Microflows$ActionActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$CommitAction + +- name: allow loop with submicroflow call chain with no commits + allow: true + input: + $Type: Microflow$Microflow + Name: MainMicroflow + MainFunction: + - Attributes: + $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$MicroflowCallAction + MicroflowName: Module1.SubmicroflowA + files: + "Module1.SubmicroflowA$Microflow.yaml": + $Type: Microflow$Microflow + Name: SubmicroflowA + MainFunction: + - Attributes: + $Type: Microflows$ActionActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$MicroflowCallAction + MicroflowName: Module2.SubmicroflowB + "Module2.SubmicroflowB$Microflow.yaml": + $Type: Microflow$Microflow + Name: SubmicroflowB + MainFunction: + - Attributes: + $Type: Microflows$ActionActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$LogMessage