Skip to content

Commit f8d6ee0

Browse files
committed
implement beta parameter
1 parent 0d688dc commit f8d6ee0

File tree

3 files changed

+164
-191
lines changed

3 files changed

+164
-191
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,12 @@ Customize the scan behavior using the following options:
4343

4444
--json set output format as json.
4545

46+
-z, --betamode a runtime toggle to enable experimental beta rules.
47+
4648
--loglevel=(trace|debug|info|warn|error|fatal) [default: warn] logging level.
4749
```
4850

49-
**Privacy:** Zero user data collected. All processing is client-side.
50-
→ See Data Handling in our [Security Policy](https://github.com/Flow-Scanner/lightning-flow-scanner-cli?tab=security-ov-file).
51+
**Privacy:** Zero user data collected. All processing is client-side. → See Data Handling in our [Security Policy](https://github.com/Flow-Scanner/lightning-flow-scanner-cli?tab=security-ov-file).
5152

5253
---
5354

src/commands/flow/scan.ts

Lines changed: 135 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
import { SfCommand, Flags } from "@salesforce/sf-plugins-core";
22
import { Messages } from "@salesforce/core";
33
import chalk from "chalk";
4-
54
import { loadScannerOptions } from "../../libs/ScannerConfig.js";
65
import { FindFlows } from "../../libs/FindFlows.js";
76
import { ScanResult as Output } from "../../models/ScanResult.js";
8-
97
import pkg, {
108
ParsedFlow,
119
ScanResult,
1210
RuleResult,
1311
ResultDetails,
1412
} from "@flow-scanner/lightning-flow-scanner-core";
1513
import { inspect } from "util";
16-
const { parse: parseFlows, scan: scanFlows } = pkg;
1714

18-
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
15+
const {
16+
parse: parseFlows,
17+
scan: scanFlows,
18+
} = pkg;
1919

20+
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
2021
const messages = Messages.loadMessages("lightning-flow-scanner", "command");
2122

2223
export default class Scan extends SfCommand<Output> {
@@ -37,7 +38,6 @@ export default class Scan extends SfCommand<Output> {
3738
public static requiresProject = false;
3839
protected static supportsUsername = true;
3940

40-
protected userConfig: object;
4141
protected failOn = "error";
4242
protected errorCounters: Map<string, number> = new Map<string, number>();
4343

@@ -61,208 +61,169 @@ export default class Scan extends SfCommand<Output> {
6161
options: ["error", "warning", "note", "never"] as const,
6262
default: "error",
6363
})(),
64-
retrieve: Flags.boolean({
65-
char: "r",
66-
description: "Force retrieve Flows from org at the start of the command",
67-
default: false,
68-
}),
6964
files: Flags.file({
7065
multiple: true,
7166
exists: true,
7267
description: "List of source flows paths to scan",
7368
charAliases: ["p"],
7469
exclusive: ["directory"],
75-
})
70+
}),
71+
betamode: Flags.boolean({
72+
char: "z",
73+
description: "Enable beta rules at run-time (experimental)",
74+
default: false,
75+
}),
7676
};
7777

78-
public async run(): Promise<Output> {
79-
this.enforceSecurityGuards();
80-
const { flags } = await this.parse(Scan);
81-
this.failOn = flags.failon || "error";
82-
83-
this.spinner.start("Loading Lightning Flow Scanner");
84-
this.userConfig = await loadScannerOptions(flags.config);
78+
public async run(): Promise<Output> {
79+
const { flags } = await this.parse(Scan);
80+
this.failOn = flags.failon || "error";
8581

86-
const targets: string[] = flags.files;
82+
this.spinner.start("Loading Lightning Flow Scanner");
8783

88-
// Step 2: Find flows to scan
89-
const flowFiles = this.findFlows(flags.directory, targets);
90-
this.spinner.start(`Identified ${flowFiles.length} flows to scan`);
84+
// ---- 1. Load config file -------------------------------------------------
85+
const fileConfig = await loadScannerOptions(flags.config);
9186

92-
// Step 3: Parse flows
93-
const parsedFlows: ParsedFlow[] = await parseFlows(flowFiles);
94-
this.debug(`parsed flows ${parsedFlows.length}`, ...parsedFlows);
87+
// ---- 2. Merge CLI overrides (betamode) ----------------------------------
88+
const mergedConfig = {
89+
...fileConfig,
90+
betamode: flags.betamode ?? fileConfig.betamode ?? false,
91+
};
9592

96-
// Step 4: Run scan safely
97-
const tryScan = (): [ScanResult[], error: Error] => {
98-
try {
99-
const scanResult =
100-
this.userConfig && Object.keys(this.userConfig).length > 0
101-
? scanFlows(parsedFlows, this.userConfig)
102-
: scanFlows(parsedFlows);
103-
return [scanResult, null];
104-
} catch (error) {
105-
return [null, error];
106-
}
107-
};
93+
// ---- 3. Locate flows ----------------------------------------------------
94+
const flowFiles = this.findFlows(flags.directory, flags.files);
95+
this.spinner.start(`Identified ${flowFiles.length} flows to scan`);
96+
97+
// ---- 4. Parse flows ------------------------------------------------------
98+
const parsedFlows: ParsedFlow[] = await parseFlows(flowFiles);
99+
this.debug(`parsed flows ${parsedFlows.length}`, ...parsedFlows);
100+
101+
// ---- 5. Run the scan ----------------------------------------------------
102+
const tryScan = (): [ScanResult[], Error | null] => {
103+
try {
104+
const scanConfig: any = {
105+
rules: mergedConfig.rules ?? {},
106+
betamode: !!mergedConfig.betamode,
107+
};
108+
return [scanFlows(parsedFlows, scanConfig), null];
109+
} catch (err) {
110+
return [null, err as Error];
111+
}
112+
};
113+
const [scanResults, scanError] = tryScan();
108114

109-
const [scanResults, error] = tryScan();
110-
this.debug(`use new scan? ${process.env.IS_NEW_SCAN_ENABLED}`);
111-
this.debug(`error:`, inspect(error));
112-
this.debug(`scan results: ${scanResults.length}`, ...scanResults);
113-
this.spinner.stop(`Scan complete`);
115+
this.debug(`error:`, inspect(scanError));
116+
this.debug(`scan results: ${scanResults.length}`, ...scanResults);
117+
this.spinner.stop(`Scan complete`);
114118

115-
// Step 5: Build and display results
116-
const results = this.buildResults(scanResults);
117-
if (results.length > 0) {
118-
const resultsByFlow = {};
119-
for (const result of results) {
120-
resultsByFlow[result.flowName] = resultsByFlow[result.flowName] || [];
121-
resultsByFlow[result.flowName].push(result);
122-
}
123-
for (const resultKey in resultsByFlow) {
124-
const matchingScanResult = scanResults.find(
125-
(res) => res.flow.label === resultKey
126-
);
127-
this.styledHeader(
128-
`Flow: ${chalk.yellow(resultKey)} ${chalk.bgYellow(
129-
`(${matchingScanResult.flow.name}.flow-meta.xml)`
130-
)} ${chalk.red(
131-
`(${resultsByFlow[resultKey].length} results)`
132-
)}`
133-
);
134-
this.log(chalk.italic("Type: " + matchingScanResult.flow.type));
135-
this.log("");
136-
this.table({
137-
data: resultsByFlow[resultKey],
138-
columns: ["rule", "type", "name", "severity"],
139-
});
140-
this.debug(`Results By Flow: ${inspect(resultsByFlow[resultKey])}`);
141-
this.log("");
119+
// ---- 6. Build / display results -----------------------------------------
120+
const results = this.buildResults(scanResults);
121+
122+
if (results.length > 0) {
123+
const resultsByFlow: Record<string, any[]> = {};
124+
for (const r of results) {
125+
resultsByFlow[r.flowName] = resultsByFlow[r.flowName] ?? [];
126+
resultsByFlow[r.flowName].push(r);
127+
}
128+
129+
for (const flowName in resultsByFlow) {
130+
const match = scanResults.find((s) => s.flow.label === flowName)!;
131+
this.styledHeader(
132+
`Flow: ${chalk.yellow(flowName)} ${chalk.bgYellow(
133+
`(${match.flow.name}.flow-meta.xml)`
134+
)} ${chalk.red(`(${resultsByFlow[flowName].length} results)`)}`
135+
);
136+
this.log(chalk.italic("Type: " + match.flow.type));
137+
this.log("");
138+
this.table({
139+
data: resultsByFlow[flowName],
140+
columns: ["rule", "type", "name", "severity"],
141+
});
142+
this.debug(`Results By Flow: ${inspect(resultsByFlow[flowName])}`);
143+
this.log("");
144+
}
142145
}
143-
}
144146

145-
this.styledHeader(
146-
`Total: ${chalk.red(results.length + " Results")} in ${chalk.yellow(
147-
scanResults.length + " Flows"
148-
)}.`
149-
);
147+
this.styledHeader(
148+
`Total: ${chalk.red(results.length + " Results")} in ${chalk.yellow(
149+
scanResults.length + " Flows"
150+
)}.`
151+
);
150152

151-
// Step 6: Display summary
152-
for (const severity of ["error", "warning", "note"]) {
153-
const severityCounter = this.errorCounters[severity] || 0;
154-
this.log(`- ${severity}: ${severityCounter}`);
155-
}
156-
this.log("");
153+
for (const sev of ["error", "warning", "note"]) {
154+
const cnt = this.errorCounters.get(sev) ?? 0;
155+
this.log(`- ${sev}: ${cnt}`);
156+
}
157+
this.log("");
157158

158-
// Step 7: Return status and summary
159-
const status = this.getStatus();
160-
if (status > 0) {
161-
process.exitCode = status;
162-
}
159+
// ---- 7. Exit code -------------------------------------------------------
160+
const status = this.getStatus();
161+
if (status > 0) process.exitCode = status;
163162

164-
const summary = {
165-
flowsNumber: scanResults.length,
166-
results: results.length,
167-
message: `A total of ${results.length} results have been found in ${scanResults.length} flows.`,
168-
errorLevelsDetails: {},
169-
};
163+
const summary = {
164+
flowsNumber: scanResults.length,
165+
results: results.length,
166+
message: `A total of ${results.length} results have been found in ${scanResults.length} flows.`,
167+
errorLevelsDetails: {},
168+
};
170169

171-
return { summary, status: status, results };
172-
}
170+
return { summary, status, results };
171+
}
173172

174-
private findFlows(directory: string, sourcepath: string[]) {
175-
// List flows that will be scanned
176-
let flowFiles;
177-
if (directory) {
178-
flowFiles = FindFlows(directory);
179-
} else if (sourcepath) {
180-
flowFiles = sourcepath;
181-
} else {
182-
flowFiles = FindFlows(".");
183-
}
184-
return flowFiles;
173+
private findFlows(directory?: string, sourcepath?: string[]) {
174+
if (directory) return FindFlows(directory);
175+
if (sourcepath?.length) return sourcepath;
176+
return FindFlows(".");
185177
}
186178

187179
private getStatus() {
188-
let status = 0;
189-
if (this.failOn === "never") {
190-
status = 0;
191-
} else {
192-
if (this.failOn === "error" && this.errorCounters["error"] > 0) {
193-
status = 1;
194-
} else if (
195-
this.failOn === "warning" &&
196-
(this.errorCounters["error"] > 0 || this.errorCounters["warning"] > 0)
197-
) {
198-
status = 1;
199-
} else if (
200-
this.failOn === "note" &&
201-
(this.errorCounters["error"] > 0 ||
202-
this.errorCounters["warning"] > 0 ||
203-
this.errorCounters["note"] > 0)
204-
) {
205-
status = 1;
206-
}
207-
}
208-
return status;
180+
if (this.failOn === "never") return 0;
181+
if (this.failOn === "error" && this.errorCounters.get("error")! > 0) return 1;
182+
if (
183+
this.failOn === "warning" &&
184+
(this.errorCounters.get("error")! > 0 || this.errorCounters.get("warning")! > 0)
185+
)
186+
return 1;
187+
if (
188+
this.failOn === "note" &&
189+
(this.errorCounters.get("error")! > 0 ||
190+
this.errorCounters.get("warning")! > 0 ||
191+
this.errorCounters.get("note")! > 0)
192+
)
193+
return 1;
194+
return 0;
209195
}
210196

211-
private buildResults(scanResults) {
212-
const errors = [];
213-
for (const scanResult of scanResults) {
214-
const flowName = scanResult.flow.label;
215-
const flowType = scanResult.flow.type[0];
216-
for (const ruleResult of scanResult.ruleResults as RuleResult[]) {
217-
const ruleDescription = ruleResult.ruleDefinition.description;
218-
const rule = ruleResult.ruleDefinition.label;
219-
if (
220-
ruleResult.occurs &&
221-
ruleResult.details &&
222-
ruleResult.details.length > 0
223-
) {
224-
const severity = ruleResult.severity || "error";
225-
const flowUri = scanResult.flow.fsPath;
226-
const flowApiName = `${scanResult.flow.name}.flow-meta.xml`;
227-
for (const result of ruleResult.details as ResultDetails[]) {
228-
const detailObj = Object.assign(result, {
229-
ruleDescription,
230-
rule,
197+
private buildResults(scanResults: ScanResult[]) {
198+
const errors: any[] = [];
199+
200+
for (const sr of scanResults) {
201+
const flowName = sr.flow.label;
202+
const flowType = sr.flow.type[0];
203+
204+
for (const rule of sr.ruleResults as RuleResult[]) {
205+
if (!rule.occurs || !rule.details?.length) continue;
206+
207+
const severity = rule.severity ?? "error";
208+
const flowUri = sr.flow.fsPath;
209+
const flowApiName = `${sr.flow.name}.flow-meta.xml`;
210+
211+
for (const detail of rule.details as ResultDetails[]) {
212+
errors.push(
213+
Object.assign(detail, {
214+
ruleDescription: rule.ruleDefinition.description,
215+
rule: rule.ruleDefinition.label,
231216
flowName,
232217
flowType,
233218
severity,
234219
flowUri,
235220
flowApiName,
236-
});
237-
errors.push(detailObj);
238-
this.errorCounters[severity] =
239-
(this.errorCounters[severity] || 0) + 1;
240-
}
221+
})
222+
);
223+
this.errorCounters.set(severity, (this.errorCounters.get(severity) ?? 0) + 1);
241224
}
242225
}
243226
}
244227
return errors;
245228
}
246-
247-
private enforceSecurityGuards(): void {
248-
// 🔒 Monkey-patch eval
249-
(global as any).eval = function (): never {
250-
throw new Error("Blocked use of eval() in lightning-flow-scanner-core");
251-
};
252-
253-
// 🔒 Monkey-patch Function constructor
254-
(global as any).Function = function (): never {
255-
throw new Error("Blocked use of Function constructor in lightning-flow-scanner-core");
256-
};
257-
258-
// 🔒 Intercept dynamic import() calls
259-
const dynamicImport = (globalThis as any).import;
260-
(globalThis as any).import = async (...args: any[]): Promise<any> => {
261-
const specifier = args[0];
262-
if (typeof specifier === "string" && specifier.startsWith("http")) {
263-
throw new Error(`Blocked remote import: ${specifier}`);
264-
}
265-
return dynamicImport(...args);
266-
};
267-
}
268-
}
229+
}

0 commit comments

Comments
 (0)