11import { SfCommand , Flags } from "@salesforce/sf-plugins-core" ;
22import { Messages } from "@salesforce/core" ;
33import chalk from "chalk" ;
4-
54import { loadScannerOptions } from "../../libs/ScannerConfig.js" ;
65import { FindFlows } from "../../libs/FindFlows.js" ;
76import { ScanResult as Output } from "../../models/ScanResult.js" ;
8-
97import pkg , {
108 ParsedFlow ,
119 ScanResult ,
1210 RuleResult ,
1311 ResultDetails ,
1412} from "@flow-scanner/lightning-flow-scanner-core" ;
1513import { 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 ) ;
2021const messages = Messages . loadMessages ( "lightning-flow-scanner" , "command" ) ;
2122
2223export 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