Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,30 @@
"name": "Run npm test",
"request": "launch",
"type": "node-terminal"
},
{
"type": "node",
"name": "vscode-jest-tests.v2.lightning-flow-scanner-core",
"request": "launch",
"args": [
"test",
"--",
"--runInBand",
"--watchAll=false",
"--testNamePattern",
"${jest.testNamePattern}",
"--runTestsByPath",
"${jest.testFile}"
],
"cwd": "/Users/jun/Development/github/lightning-flow-scanner-core",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"runtimeExecutable": "npm",
"env": {
"OVERRIDE_CONFIG": "true",
"IS_NEW_SCAN_ENABLED": "true"
}
}
]
}
10 changes: 10 additions & 0 deletions src/main/interfaces/AdvancedRuleConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type AdvancedConfig = {
disabled?: boolean;
path?: string;
severity?: string;
suppressions?: string[];
};

export type AdvancedRuleConfig = {
[ruleName: string]: AdvancedConfig;
};
7 changes: 7 additions & 0 deletions src/main/interfaces/AdvancedRuleDefintion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { AdvancedRuleConfig } from "./AdvancedRuleConfig";

import { Flow, RuleResult } from "../internals/internals";

export interface AdvancedRuleDefinition {
execute(flow: Flow, ruleConfiguration?: AdvancedRuleConfig): RuleResult;
}
6 changes: 6 additions & 0 deletions src/main/interfaces/AdvancedSuppression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { RuleResult } from "../internals/internals";
import { AdvancedRuleConfig } from "./AdvancedRuleConfig";

export interface AdvancedSuppression {
suppress(scanResult: RuleResult, ruleConfiguration?: AdvancedRuleConfig): RuleResult;
}
3 changes: 2 additions & 1 deletion src/main/interfaces/IRulesConfig.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { AdvancedRuleConfig } from "./AdvancedRuleConfig";
import { IExceptions } from "./IExceptions";
import { IRuleOptions } from "./IRuleOptions";

export interface IRulesConfig {
rules?: IRuleOptions;
exceptions?: IExceptions;
rules?: AdvancedRuleConfig | IRuleOptions;
}
7 changes: 4 additions & 3 deletions src/main/libs/DynamicRule.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { IRuleDefinition } from "../interfaces/IRuleDefinition";
import { AdvancedRule } from "../models/AdvancedRule";
import { BetaRuleStore, DefaultRuleStore } from "../store/DefaultRuleStore";

export class DynamicRule {
export class DynamicRule<T extends AdvancedRule | IRuleDefinition> {
constructor(className: string) {
if (!DefaultRuleStore.hasOwnProperty(className) && BetaRuleStore.hasOwnProperty(className)) {
return new BetaRuleStore[className]() as IRuleDefinition;
return new BetaRuleStore[className]() as T;
}
return new DefaultRuleStore[className]() as IRuleDefinition;
return new DefaultRuleStore[className]() as T;
}
}
122 changes: 98 additions & 24 deletions src/main/libs/ScanFlows.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
import { GetRuleDefinitions } from "./GetRuleDefinitions";
import * as core from "../../main/internals/internals";
import type { IRuleDefinition } from "../interfaces/IRuleDefinition";

import {
Flow,
IRulesConfig,
ResultDetails,
RuleResult,
ScanResult,
} from "../../main/internals/internals";
import { AdvancedRuleConfig } from "../interfaces/AdvancedRuleConfig";
import { AdvancedRule } from "../models/AdvancedRule";
import { ParsedFlow } from "../models/ParsedFlow";
import { BetaRuleStore, DefaultRuleStore } from "../store/DefaultRuleStore";
import { DynamicRule } from "./DynamicRule";
import { GetRuleDefinitions } from "./GetRuleDefinitions";

export function scan(
parsedFlows: ParsedFlow[],
ruleOptions?: core.IRulesConfig
): core.ScanResult[] {
const flows: core.Flow[] = [];
const { IS_NEW_SCAN_ENABLED: isNewScanEnabled, OVERRIDE_CONFIG: overrideConfig } = process.env;

// Will be replaced by scanInternal in the future
// eslint-disable-next-line sonarjs/cognitive-complexity
export function scan(parsedFlows: ParsedFlow[], ruleOptions?: IRulesConfig): ScanResult[] {
if (isNewScanEnabled === "true") {
return scanInternal(parsedFlows, ruleOptions);
}
const flows: Flow[] = [];
for (const flow of parsedFlows) {
if (!flow.errorMessage && flow.flow) {
flows.push(flow.flow);
}
}
let scanResults: core.ScanResult[];
let scanResults: ScanResult[];
if (ruleOptions?.rules && Object.entries(ruleOptions.rules).length > 0) {
scanResults = ScanFlows(flows, ruleOptions);
} else {
Expand All @@ -23,14 +39,12 @@ export function scan(
for (const [exceptionName, exceptionElements] of Object.entries(ruleOptions.exceptions)) {
for (const scanResult of scanResults) {
if (scanResult.flow.name === exceptionName) {
for (const ruleResult of scanResult.ruleResults as core.RuleResult[]) {
for (const ruleResult of scanResult.ruleResults as RuleResult[]) {
if (exceptionElements[ruleResult.ruleName]) {
const exceptions = exceptionElements[ruleResult.ruleName];
const filteredDetails = (ruleResult.details as core.ResultDetails[]).filter(
(detail) => {
return !exceptions.includes(detail.name);
}
);
const filteredDetails = (ruleResult.details as ResultDetails[]).filter((detail) => {
return !exceptions.includes(detail.name);
});
ruleResult.details = filteredDetails;
ruleResult.occurs = filteredDetails.length > 0;
}
Expand All @@ -43,10 +57,12 @@ export function scan(
return scanResults;
}

export function ScanFlows(flows: core.Flow[], ruleOptions?: core.IRulesConfig): core.ScanResult[] {
const flowResults: core.ScanResult[] = [];
// Will be removed once scanInternal is fully enabled
// eslint-disable-next-line sonarjs/cognitive-complexity
export function ScanFlows(flows: Flow[], ruleOptions?: IRulesConfig): ScanResult[] {
const flowResults: ScanResult[] = [];

let selectedRules: core.IRuleDefinition[] = [];
let selectedRules: IRuleDefinition[] = [];
if (ruleOptions && ruleOptions.rules) {
const ruleMap = new Map<string, object>();
for (const [ruleName, rule] of Object.entries(ruleOptions.rules)) {
Expand All @@ -58,7 +74,7 @@ export function ScanFlows(flows: core.Flow[], ruleOptions?: core.IRulesConfig):
}

for (const flow of flows) {
const ruleResults: core.RuleResult[] = [];
const ruleResults: RuleResult[] = [];
for (const rule of selectedRules) {
try {
if (rule.supportedTypes.includes(flow.type)) {
Expand All @@ -75,17 +91,75 @@ export function ScanFlows(flows: core.Flow[], ruleOptions?: core.IRulesConfig):
}
ruleResults.push(result);
} else {
ruleResults.push(new core.RuleResult(rule, []));
ruleResults.push(new RuleResult(rule, []));
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
const message =
"Something went wrong while executing " + rule.name + " in the Flow: '" + flow.name;
ruleResults.push(new core.RuleResult(rule, [], message));
const message = `Something went wrong while executing ${rule.name} in the Flow: ${flow.name} with error ${error}`;
ruleResults.push(new RuleResult(rule, [], message));
}
}
flowResults.push(new core.ScanResult(flow, ruleResults));
flowResults.push(new ScanResult(flow, ruleResults));
}

return flowResults;
}

export function scanInternal(parsedFlows: ParsedFlow[], ruleOptions?: IRulesConfig): ScanResult[] {
const flows: Flow[] = parsedFlows.map((parsedFlow) => parsedFlow.flow as Flow);
const scanResults: ScanResult[] = [];
for (const flow of flows) {
scanResults.push(scanFlowWithConfig(flow, ruleOptions));
}
return scanResults;
}

function ruleAndConfig(
ruleOptions?: IRulesConfig
): [Record<string, AdvancedRule>, Record<string, AdvancedRuleConfig>] {
// for unit tests, use a small set of rules
const ruleConfiguration = unifiedRuleConfig(ruleOptions);
let allRules: Record<string, AdvancedRule> = { ...DefaultRuleStore, ...BetaRuleStore };
if (overrideConfig === "true" && ruleOptions?.rules) {
allRules = Object.entries(allRules).reduce<Record<string, AdvancedRule>>(
(accumulator, [ruleName, rule]) => {
if (ruleOptions?.rules?.[ruleName]) {
accumulator[ruleName] = rule;
}
return accumulator;
},
{}
);
}
return [allRules, ruleConfiguration];
}

function scanFlowWithConfig(flow: Flow, ruleOptions?: IRulesConfig): ScanResult {
const [allRules, ruleConfiguration] = ruleAndConfig(ruleOptions);
const ruleResults: RuleResult[] = [];
for (const [ruleName] of Object.entries(allRules)) {
const advancedRule = new DynamicRule<AdvancedRule>(ruleName);
ruleResults.push(
(advancedRule as AdvancedRule).execute(flow, ruleConfiguration[ruleName] ?? {})
);
}
return new ScanResult(flow, ruleResults);
}

function unifiedRuleConfig(
ruleOptions: IRulesConfig | undefined
): Record<string, AdvancedRuleConfig> {
const configuredRules: AdvancedRuleConfig = ruleOptions?.rules ?? {};
const activeConfiguredRules: Record<string, AdvancedRuleConfig> = Object.entries(configuredRules)
.filter(([, configuration]) => {
if (!("disabled" in configuration)) {
return true;
}

return configuration.disabled !== true;
})
.reduce<Record<string, AdvancedRuleConfig>>((accumulator, [ruleName, config]) => {
return { ...accumulator, [ruleName]: config as AdvancedRuleConfig };
}, {});

return activeConfiguredRules;
}
44 changes: 44 additions & 0 deletions src/main/models/AdvancedRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { AdvancedRuleConfig } from "../interfaces/AdvancedRuleConfig";
import { AdvancedRuleDefinition } from "../interfaces/AdvancedRuleDefintion";
import { AdvancedSuppression } from "../interfaces/AdvancedSuppression";
import { IRuleDefinition } from "../internals/internals";
import { Flow } from "./Flow";
import { RuleCommon } from "./RuleCommon";
import { RuleInfo } from "./RuleInfo";
import { RuleResult } from "./RuleResult";

export abstract class AdvancedRule extends RuleCommon {
constructor(
info: RuleInfo,
optional?: {
severity?: string;
}
) {
super(info, optional);
}

public execute(flow: Flow, ruleConfiguration?: AdvancedRuleConfig): RuleResult {
if (!hasAdvancedRuleDefinition(this)) {
return new RuleResult(this as unknown as IRuleDefinition, []);
}

let ruleResult = (this as AdvancedRuleDefinition).execute(flow, ruleConfiguration);

if (hasAdvancedSuppression(this)) {
ruleResult = this.suppress(ruleResult, ruleConfiguration);
}

return ruleResult;
}
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const isFunction = (val: unknown): val is Function => typeof val === "function";

function hasAdvancedRuleDefinition(instance: unknown): instance is AdvancedRuleDefinition {
return isFunction((instance as AdvancedRuleDefinition).execute);
}

function hasAdvancedSuppression(instance: unknown): instance is AdvancedSuppression {
return isFunction((instance as AdvancedSuppression).suppress);
}
10 changes: 5 additions & 5 deletions src/main/models/RuleCommon.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { RuleInfo } from "./RuleInfo";

export class RuleCommon {
public autoFixable: boolean;
public description: string;
public docRefs: Array<{ label: string; path: string }> = [];
public isConfigurable: boolean;
public label;
public name;
public severity?;
public uri;
public docRefs: { label: string; path: string }[] = [];
public description: string;
public supportedTypes: string[];
public isConfigurable: boolean;
public autoFixable: boolean;
public uri;

constructor(
info: RuleInfo,
Expand Down
22 changes: 20 additions & 2 deletions src/main/rules/MissingFaultPath.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { AdvancedConfig } from "../interfaces/AdvancedRuleConfig";
import { AdvancedSuppression } from "../interfaces/AdvancedSuppression";
import * as core from "../internals/internals";
import { RuleCommon } from "../models/RuleCommon";

export class MissingFaultPath extends RuleCommon implements core.IRuleDefinition {
export class MissingFaultPath
extends RuleCommon
implements AdvancedSuppression, core.IRuleDefinition
{
protected applicableElements: string[] = [
"recordLookups",
"recordDeletes",
Expand All @@ -28,7 +33,6 @@ export class MissingFaultPath extends RuleCommon implements core.IRuleDefinition
supportedTypes: [...core.FlowType.backEndTypes, ...core.FlowType.visualTypes],
});
}

public execute(flow: core.Flow): core.RuleResult {
const compiler = new core.Compiler();
const results: core.ResultDetails[] = [];
Expand Down Expand Up @@ -63,6 +67,20 @@ export class MissingFaultPath extends RuleCommon implements core.IRuleDefinition
return new core.RuleResult(this, results);
}

public suppress(
scanResult: core.RuleResult,
ruleConfiguration?: AdvancedConfig
): core.RuleResult {
const suppressedResults: core.ResultDetails[] = [];
for (const resultDetails of scanResult.details) {
if (ruleConfiguration?.suppressions?.includes(resultDetails.name)) {
continue;
}
suppressedResults.push(resultDetails);
}
return new core.RuleResult(this, suppressedResults);
}

private isPartOfFaultHandlingFlow(element: core.FlowNode, flow: core.Flow): boolean {
const flowelements = flow.elements?.filter(
(el) => el instanceof core.FlowNode
Expand Down