Skip to content

Commit 16032c8

Browse files
committed
enforced security guard
1 parent abc0468 commit 16032c8

File tree

1 file changed

+131
-108
lines changed

1 file changed

+131
-108
lines changed

src/commands/flow/scan.ts

Lines changed: 131 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -83,128 +83,128 @@ export default class Scan extends SfCommand<Output> {
8383
}),
8484
};
8585

86-
public async run(): Promise<Output> {
87-
const { flags } = await this.parse(Scan);
88-
this.failOn = flags.failon || "error";
89-
this.spinner.start("Loading Lightning Flow Scanner");
90-
this.userConfig = await loadScannerOptions(flags.config);
91-
if (flags.targetusername) {
92-
await this.retrieveFlowsFromOrg(flags.targetusername);
93-
}
86+
public async run(): Promise<Output> {
87+
// 🚨 Step 0: Block risky APIs before anything else
88+
this.enforceSecurityGuards();
9489

95-
const targets: string[] = flags.files;
90+
// Step 1: Parse CLI flags
91+
const { flags } = await this.parse(Scan);
92+
this.failOn = flags.failon || "error";
93+
94+
this.spinner.start("Loading Lightning Flow Scanner");
95+
this.userConfig = await loadScannerOptions(flags.config);
9696

97-
const flowFiles = this.findFlows(flags.directory, targets);
98-
this.spinner.start(`Identified ${flowFiles.length} flows to scan`);
99-
// to
100-
// core.Flow
101-
const parsedFlows: ParsedFlow[] = await parseFlows(flowFiles);
102-
this.debug(`parsed flows ${parsedFlows.length}`, ...parsedFlows);
97+
if (flags.targetusername) {
98+
await this.retrieveFlowsFromOrg(flags.targetusername);
99+
}
103100

104-
const tryScan = (): [ScanResult[], error: Error] => {
105-
try {
106-
const scanResult =
107-
this.userConfig && Object.keys(this.userConfig).length > 0
108-
? scanFlows(parsedFlows, this.userConfig)
109-
: scanFlows(parsedFlows);
110-
return [scanResult, null];
111-
} catch (error) {
112-
return [null, error];
113-
}
114-
};
101+
const targets: string[] = flags.files;
115102

116-
const [scanResults, error] = tryScan();
117-
this.debug(`use new scan? ${process.env.IS_NEW_SCAN_ENABLED}`);
118-
this.debug(`error:`, inspect(error));
119-
this.debug(`scan results: ${scanResults.length}`, ...scanResults);
120-
this.spinner.stop(`Scan complete`);
103+
// Step 2: Find flows to scan
104+
const flowFiles = this.findFlows(flags.directory, targets);
105+
this.spinner.start(`Identified ${flowFiles.length} flows to scan`);
121106

122-
// Build results
123-
const results = this.buildResults(scanResults);
107+
// Step 3: Parse flows
108+
const parsedFlows: ParsedFlow[] = await parseFlows(flowFiles);
109+
this.debug(`parsed flows ${parsedFlows.length}`, ...parsedFlows);
124110

125-
if (results.length > 0) {
126-
const resultsByFlow = {};
127-
for (const result of results) {
128-
resultsByFlow[result.flowName] = resultsByFlow[result.flowName] || [];
129-
resultsByFlow[result.flowName].push(result);
130-
}
131-
for (const resultKey in resultsByFlow) {
132-
const matchingScanResult = scanResults.find((res) => {
133-
return res.flow.label === resultKey;
134-
});
135-
this.styledHeader(
136-
"Flow: " +
137-
chalk.yellow(resultKey) +
138-
" " +
139-
chalk.bgYellow(`(${matchingScanResult.flow.name}.flow-meta.xml)`) +
140-
" " +
141-
chalk.red("(" + resultsByFlow[resultKey].length + " results)"),
142-
);
143-
this.log(chalk.italic("Type: " + matchingScanResult.flow.type));
144-
this.log("");
145-
// todo flow uri
146-
//this.table(resultsByFlow[resultKey], ['rule', 'type', 'name', 'severity']);
147-
this.table({
148-
data: resultsByFlow[resultKey],
149-
columns: ["rule", "type", "name", "severity"],
150-
});
151-
this.debug(`Results By Flow:
152-
${inspect(resultsByFlow[resultKey])}`);
153-
this.log("");
154-
}
111+
// Step 4: Run scan safely
112+
const tryScan = (): [ScanResult[], error: Error] => {
113+
try {
114+
const scanResult =
115+
this.userConfig && Object.keys(this.userConfig).length > 0
116+
? scanFlows(parsedFlows, this.userConfig)
117+
: scanFlows(parsedFlows);
118+
return [scanResult, null];
119+
} catch (error) {
120+
return [null, error];
155121
}
156-
this.styledHeader(
157-
"Total: " +
158-
chalk.red(results.length + " Results") +
159-
" in " +
160-
chalk.yellow(scanResults.length + " Flows") +
161-
".",
162-
);
122+
};
163123

164-
// Display number of errors by severity
165-
for (const severity of ["error", "warning", "note"]) {
166-
const severityCounter = this.errorCounters[severity] || 0;
167-
this.log(`- ${severity}: ${severityCounter}`);
124+
const [scanResults, error] = tryScan();
125+
this.debug(`use new scan? ${process.env.IS_NEW_SCAN_ENABLED}`);
126+
this.debug(`error:`, inspect(error));
127+
this.debug(`scan results: ${scanResults.length}`, ...scanResults);
128+
this.spinner.stop(`Scan complete`);
129+
130+
// Step 5: Build and display results
131+
const results = this.buildResults(scanResults);
132+
if (results.length > 0) {
133+
const resultsByFlow = {};
134+
for (const result of results) {
135+
resultsByFlow[result.flowName] = resultsByFlow[result.flowName] || [];
136+
resultsByFlow[result.flowName].push(result);
137+
}
138+
for (const resultKey in resultsByFlow) {
139+
const matchingScanResult = scanResults.find(
140+
(res) => res.flow.label === resultKey
141+
);
142+
this.styledHeader(
143+
`Flow: ${chalk.yellow(resultKey)} ${chalk.bgYellow(
144+
`(${matchingScanResult.flow.name}.flow-meta.xml)`
145+
)} ${chalk.red(
146+
`(${resultsByFlow[resultKey].length} results)`
147+
)}`
148+
);
149+
this.log(chalk.italic("Type: " + matchingScanResult.flow.type));
150+
this.log("");
151+
this.table({
152+
data: resultsByFlow[resultKey],
153+
columns: ["rule", "type", "name", "severity"],
154+
});
155+
this.debug(`Results By Flow: ${inspect(resultsByFlow[resultKey])}`);
156+
this.log("");
168157
}
158+
}
159+
160+
this.styledHeader(
161+
`Total: ${chalk.red(results.length + " Results")} in ${chalk.yellow(
162+
scanResults.length + " Flows"
163+
)}.`
164+
);
165+
166+
// Step 6: Display summary
167+
for (const severity of ["error", "warning", "note"]) {
168+
const severityCounter = this.errorCounters[severity] || 0;
169+
this.log(`- ${severity}: ${severityCounter}`);
170+
}
169171

170-
// TODO CALL TO ACTION
171-
this.log("");
172-
this.log(
173-
chalk.bold(
174-
chalk.italic(
175-
chalk.yellowBright(
176-
"Be a part of our mission to champion Flow Best Practices by starring ⭐ us on GitHub:",
177-
),
178-
),
179-
),
180-
);
181-
this.log(
172+
this.log("");
173+
this.log(
174+
chalk.bold(
182175
chalk.italic(
183-
chalk.blueBright(
184-
chalk.underline("https://github.com/Lightning-Flow-Scanner"),
185-
),
186-
),
187-
);
176+
chalk.yellowBright(
177+
"Be a part of our mission to champion Flow Best Practices by starring ⭐ us on GitHub:"
178+
)
179+
)
180+
)
181+
);
182+
this.log(
183+
chalk.italic(
184+
chalk.blueBright(
185+
chalk.underline(
186+
"https://github.com/Lightning-Flow-Scanner"
187+
)
188+
)
189+
)
190+
);
188191

189-
const status = this.getStatus();
190-
// Set status code = 1 if there are errors, that will make cli exit with code 1 when not in --json mode
191-
if (status > 0) {
192-
process.exitCode = status;
193-
}
194-
const summary = {
195-
flowsNumber: scanResults.length,
196-
results: results.length,
197-
message:
198-
"A total of " +
199-
results.length +
200-
" results have been found in " +
201-
scanResults.length +
202-
" flows.",
203-
errorLevelsDetails: {},
204-
};
205-
return { summary, status: status, results };
192+
// Step 7: Return status and summary
193+
const status = this.getStatus();
194+
if (status > 0) {
195+
process.exitCode = status;
206196
}
207197

198+
const summary = {
199+
flowsNumber: scanResults.length,
200+
results: results.length,
201+
message: `A total of ${results.length} results have been found in ${scanResults.length} flows.`,
202+
errorLevelsDetails: {},
203+
};
204+
205+
return { summary, status: status, results };
206+
}
207+
208208
private findFlows(directory: string, sourcepath: string[]) {
209209
// List flows that will be scanned
210210
let flowFiles;
@@ -278,6 +278,29 @@ export default class Scan extends SfCommand<Output> {
278278
return errors;
279279
}
280280

281+
private enforceSecurityGuards(): void {
282+
// 🔒 Monkey-patch eval
283+
(global as any).eval = function (): never {
284+
throw new Error("Blocked use of eval() in lightning-flow-scanner-core");
285+
};
286+
287+
// 🔒 Monkey-patch Function constructor
288+
(global as any).Function = function (): never {
289+
throw new Error("Blocked use of Function constructor in lightning-flow-scanner-core");
290+
};
291+
292+
// 🔒 Intercept dynamic import() calls
293+
const dynamicImport = (globalThis as any).import;
294+
(globalThis as any).import = async (...args: any[]): Promise<any> => {
295+
const specifier = args[0];
296+
if (typeof specifier === "string" && specifier.startsWith("http")) {
297+
throw new Error(`Blocked remote import: ${specifier}`);
298+
}
299+
return dynamicImport(...args);
300+
};
301+
}
302+
303+
281304
private async retrieveFlowsFromOrg(targetusername: string) {
282305
let errored = false;
283306
this.spinner.start(chalk.yellowBright("Retrieving Metadata..."));

0 commit comments

Comments
 (0)