Sorcerer's IsleCode QueryParam Scanner / files

  1<cfcomponent output="false" displayname="qpscanner v0.7.3">
  2
  3	<cffunction name="Struct" returntype="Struct" access="private"><cfreturn Arguments/></cffunction>
  4
  5	<cffunction name="init" returntype="any" output="false" access="public">
  6		<cfargument name="jre"                   type="jre-utils"/>
  7		<cfargument name="StartingDir"           type="String"                  hint="Directory to begin scanning the contents of."/>
  8		<cfargument name="OutputFormat"          type="String"  default="html"  hint="Format of scan results: [html,wddx]"/>
  9		<cfargument name="RequestTimeout"        type="Numeric" default="-1"    hint="Override Request Timeout, -1 to ignore"/>
 10		<cfargument name="recurse"               type="Boolean" default="false" hint="Also scan sub-directories?"/>
 11		<cfargument name="Exclusions"            type="String"  default=""      hint="Exclude files & directories matching this regex."/>
 12		<cfargument name="scanOrderBy"           type="Boolean" default="true"  hint="Include ORDER BY statements in scan results?"/>
 13		<cfargument name="scanQoQ"               type="Boolean" default="true"  hint="Include Query of Queries in scan results?"/>
 14		<cfargument name="scanBuiltInFunc"       type="Boolean" default="true"  hint="Include Built-in Functions in scan results?"/>
 15		<cfargument name="showScopeInfo"         type="Boolean" default="true"  hint="Show scope information in scan results?"/>
 16		<cfargument name="highlightClientScopes" type="Boolean" default="true"  hint="Highlight scopes with greater risk?"/>
 17		<cfargument name="ClientScopes"          type="String"  default="form,url,client,cookie" hint="Scopes considered client scopes."/>
 18		<cfargument name="NumericFunctions"      type="String"  default="val,year,month,day,hour,minute,second,asc,dayofweek,dayofyear,daysinyear,quarter,week,fix,int,round,ceiling,gettickcount,len,min,max,pi,arraylen,listlen,structcount,listvaluecount,listvaluecountnocase,rand,randrange"/>
 19		<cfargument name="BuiltInFunctions"      type="String"  default="now,#Arguments.NumericFunctions#"/>
 20
 21		<cfset var Arg = -1/>
 22		<cfset var RegexList = ""/>
 23		<cfset var Rex = ""/>
 24		<cfset var cf = 'cf'/>
 25
 26		<cfloop item="Arg" collection="#Arguments#">
 27			<cfset This[Arg] = Arguments[Arg]/>
 28		</cfloop>
 29
 30		<cfset Variables.jre = Arguments.jre/>
 31		<cfset StructDelete(This,'jre')/>
 32
 33		<cfset This.Totals = Struct
 34			( AlertCount: 0
 35			, QueryCount: 0
 36			, FileCount : 0
 37			, DirCount  : 0
 38			, Time      : 0
 39			)/>
 40
 41		<cfset This.Timeout = false/>
 42
 43		<cfset Variables.ResultFields = "FileId,FileName,QueryAlertCount,QueryTotalCount,QueryId,QueryName,QueryStartLine,QueryEndLine,ScopeList,ContainsClientScope,QueryCode"/>
 44		<cfset Variables.AlertData = QueryNew(Variables.ResultFields)/>
 45
 46		<cfsavecontent variable="RegexList"><cfoutput>
 47			findQueries      |(?si)(<#cf#query[^p]).*?(?=</#cf#query>)
 48			findQueryTag     |(?si)(<#cf#query(?!p)[^>]{0,300}>)
 49			isQueryOfQuery   |(?si)dbtype\s*=\s*["']query["']
 50			killParams       |(?si)<#cf#queryparam[^>]+>
 51			killCfTag        |(?si)<#cf#[a-z]{2,}[^>]*> <!--- Deliberately excludes Custom Tags and CFX --->
 52			killOrderBy      |(?si)\bORDER BY\b.*?$
 53			killBuiltIn      |(?si)##(#ListChangeDelims(This.BuiltInFunctions,'|')#)\([^)]*\)##
 54			findScopes       |(?si)(?<=##([a-z]{1,20}\()?)[^\(##<]+?(?=\.[^##<]+?##)
 55			findName         |(?si)(?<=(<#cf#query[^>]{0,300}\bname=")).*?(?="[^>]{0,300}>)
 56			findClientScopes |(?i)\b(#ListChangeDelims(This.ClientScopes,'|')#)\b
 57			isCfmlFile       |(?i)\.cf(c|ml?)$
 58		</cfoutput></cfsavecontent>
 59
 60		<cfloop index="Rex" list="#RegexList#" delimiters="#Chr(10)#">
 61			<cfif Len(Trim(Rex))>
 62				<cfset Variables.Regexes[ Trim(ListFirst(Rex,'|')) ] = Trim(ListRest(Rex,'|'))/>
 63			</cfif>
 64		</cfloop>
 65
 66		<cfreturn This/>
 67	</cffunction>
 68
 69
 70
 71	<cffunction name="go" returntype="any" output="false" access="public">
 72		<cfset var StartTime = getTickCount()/>
 73
 74		<cfif This.RequestTimeout GT 0>
 75			<cfsetting requesttimeout="#This.RequestTimeout#"/>
 76		</cfif>
 77
 78		<cftry>
 79			<cfset scan(This.StartingDir)/>
 80
 81			<!--- TODO: MINOR: CHECK: Is this the best way to handle this? --->
 82			<!--- If timeout occurs, ignore error and proceed. --->
 83			<cfcatch>
 84				<cfif find('timeout',cfcatch.message)>
 85					<cfset This.Timeout = True/>
 86				<cfelse>
 87					<cfrethrow/>
 88				</cfif>
 89			</cfcatch>
 90		</cftry>
 91
 92		<cfset This.Totals.Time = getTickCount() - StartTime/>
 93		<cfreturn Struct
 94			( Data : Variables.AlertData
 95			, Info : Struct
 96				( Totals  : This.Totals
 97				, Timeout : This.Timeout
 98				)
 99			)/>
100	</cffunction>
101
102
103
104	<cffunction name="scan" returntype="void" output="false" access="public">
105		<cfargument name="DirName"           type="string"/>
106		<cfset var qryDir     = -1/>
107		<cfset var qryCurData = -1/>
108		<cfset var CurrentTarget = -1/>
109		<cfset var process = true/>
110		<cfset var jre = Variables.jre/>
111
112		<cfif DirectoryExists(Arguments.DirName)>
113
114			<cfdirectory
115				name="qryDir"
116				directory="#Arguments.DirName#"
117				sort="type ASC,name ASC"
118			/>
119
120			<cfloop query="qryDir">
121
122				<cfset CurrentTarget = Arguments.DirName & '/' & Name />
123
124
125				<cfset process = true/>
126				<cfloop index="CurrentExclusion" list="#This.Exclusions#" delimiters=";">
127					<cfif jre.matches( CurrentTarget , CurrentExclusion )>
128						<cfset process = false/>
129					</cfif>
130				</cfloop>
131
132				<cfif process>
133
134					<cfif (Type EQ "dir") AND This.recurse >
135						<cfset This.Totals.DirCount = This.Totals.DirCount + 1 />
136
137						<cfset scan( CurrentTarget )/>
138
139					<cfelseif jre.matches( CurrentTarget , Variables.Regexes.isCfmlFile )>
140						<cfset This.Totals.FileCount = This.Totals.FileCount + 1 />
141
142						<cfset qryCurData = hunt( CurrentTarget )/>
143
144						<cfif qryCurData.RecordCount>
145							<cfset Variables.AlertData = QueryAppend( Variables.AlertData , qryCurData )/>
146						</cfif>
147
148					</cfif>
149
150				</cfif>
151			</cfloop>
152
153		<!--- This can only potentially trigger on first iteration, if This.StartingDir is a file. --->
154		<cfelseif FileExists(Arguments.DirName)>
155			<cfset This.Totals.FileCount = This.Totals.FileCount + 1 />
156
157			<cfset qryCurData = hunt( This.StartingDir )/>
158
159			<cfif qryCurData.RecordCount>
160				<cfset Variables.AlertData = QueryAppend( Variables.AlertData , qryCurData )/>
161			</cfif>
162		</cfif>
163
164	</cffunction>
165
166
167
168
169	<cffunction name="hunt" returntype="Query" output="false">
170		<cfargument name="FileName"    type="String"/>
171		<cfset var FileData        = -1/>
172		<cfset var Matches         = -1/>
173		<cfset var i               = -1/>
174		<cfset var info            = -1/>
175		<cfset var rekCode         = -1/>
176		<cfset var QueryCode       = -1/>
177		<cfset var CurRow          = -1/>
178		<cfset var CurFileId       = -1/>
179		<cfset var StartLine       = -1/>
180		<cfset var LineCount       = -1/>
181		<cfset var BeforeQueryCode = -1/>
182		<cfset var isRisk          = -1/>
183		<cfset var UniqueToken = Chr(65536)/>
184		<cfset var qryResult   = QueryNew(Variables.ResultFields)/>
185		<cfset var REX = Variables.Regexes/>
186		<cfset var jre = Variables.jre/>
187
188
189		<cffile action="read" file="#Arguments.FileName#" variable="FileData"/>
190
191		<cfset Matches = jre.get( FileData , REX.findQueries )/>
192		<cfset This.Totals.QueryCount = This.Totals.QueryCount + ArrayLen(Matches) />
193
194		<cfloop index="i" from="1" to="#ArrayLen(Matches)#">
195
196			<cfset QueryCode = jre.replace( Matches[i] , REX.findQueryTag , '' , 'ALL' )/>
197			<cfset rekCode = duplicate(QueryCode) />
198			<cfset rekCode = jre.replace( rekCode    , REX.killParams , '' , 'ALL' )/>
199			<cfset rekCode = jre.replace( rekCode    , REX.killCfTag  , '' , 'ALL' )/>
200
201			<cfif NOT This.scanOrderBy>
202				<cfset rekCode = jre.replace( rekCode , REX.killOrderBy , '' , 'ALL' )/>
203			</cfif>
204			<cfif NOT This.scanBuiltInFunc>
205				<cfset rekCode = jre.replace( rekCode , REX.killBuiltIn , '' , 'ALL' )/>
206			</cfif>
207
208			<cfset isRisk = find( '##' , rekCode )/>
209
210
211			<cfif (NOT This.scanQoQ) AND jre.matches( Matches[i] , REX.isQueryOfQuery )>
212				<cfset isRisk = false/>
213			</cfif>
214
215
216			<cfif isRisk>
217				<cfset CurRow = QueryAddRow(qryResult)/>
218
219				<cfset qryResult.QueryCode[CurRow] = jre.replace( QueryCode , Chr(13) , Chr(10) , 'all' ) />
220				<cfset qryResult.QueryCode[CurRow] = jre.replace( qryResult.QueryCode[CurRow] , Chr(10)&Chr(10) , Chr(10) , 'all' ) />
221				<cfif This.showScopeInfo >
222					<cfset qryResult.ScopeList[CurRow] = ArrayToList( ArrayUnique( jre.get( rekCode , REX.findScopes ) ) ) />
223
224					<cfset qryResult.ContainsClientScope[CurRow] = false/>
225					<cfif This.highlightClientScopes>
226						<cfloop index="CurrentScope" list="#This.ClientScopes#">
227							<cfif ListFind( qryResult.ScopeList[CurRow] , CurrentScope )>
228								<cfset qryResult.ContainsClientScope[CurRow] = true/>
229								<cfbreak/>
230							</cfif>
231						</cfloop>
232					</cfif>
233				</cfif>
234
235				<!--- CF8 doesn't support get()[1] so need to use two lines: --->
236				<cfset QueryTagCode = jre.get( Matches[i] , REX.findQueryTag )/>
237				<cfset QueryTagCode = QueryTagCode[1] />
238
239				<cfset BeforeQueryCode = ListFirst ( replace ( ' '&FileData&' ' , Matches[i] , UniqueToken ) , UniqueToken )/>
240
241
242				<cfset StartLine = 1+ArrayLen( jre.get( BeforeQueryCode , chr(10) ) )/>
243				<cfset LineCount = ArrayLen( jre.get( Matches[i] , chr(10) ) )/>
244
245
246				<cfset qryResult.QueryStartLine[CurRow] = StartLine/>
247				<cfset qryResult.QueryEndLine[CurRow]   = StartLine + LineCount />
248				<cfset qryResult.QueryName[CurRow]      = ArrayToList( jre.get( ListLast(QueryTagCode,chr(10)) , REX.findName ) )/>
249				<cfset qryResult.QueryId[CurRow]        = createUuid() />
250				<cfif NOT Len( qryResult.QueryName[CurRow] )>
251					<cfset qryResult.QueryName[CurRow] = "[unknown]"/>
252				</cfif>
253
254			</cfif>
255
256		</cfloop>
257
258		<cfset CurFileId = createUUID()/>
259		<cfloop query="qryResult">
260			<cfset qryResult.FileId[qryResult.CurrentRow]          = CurFileId />
261			<cfset qryResult.FileName[qryResult.CurrentRow]        = Arguments.FileName />
262			<cfset qryResult.QueryTotalCount[qryResult.CurrentRow] = ArrayLen(Matches) />
263			<cfset qryResult.QueryAlertCount[qryResult.CurrentRow] = qryResult.RecordCount />
264		</cfloop>
265		<cfset This.Totals.AlertCount = This.Totals.AlertCount + qryResult.RecordCount />
266
267		<cfreturn qryResult/>
268	</cffunction>
269
270
271
272
273
274
275
276
277	<cffunction name="ArrayUnique" returntype="Array" output="false" access="private">
278		<cfargument name="ArrayVar" type="Array"/>
279		<cfset var UniqueToken = Chr(65536)/>
280		<cfset var Result = duplicate(Arguments.ArrayVar)/>
281		<cfset ArraySort(Result,'text')/>
282		<cfset Result = ArrayToList( Result , UniqueToken )/>
283		<!--- TODO: MINOR: FIX: Using \b works for the ScopeList, but is not good enough for general use - why not using UniqueToken? --->
284		<cfset Result = REreplace( Result & UniqueToken , '(\b(.*?)\b)\1+' , '\1' , 'all' )/>
285		<cfset Result = ListToArray( Result , UniqueToken )/>
286		<!--- TODO: MINOR: Ideally, the original array order should be restored. --->
287		<cfreturn Result/>
288	</cffunction>
289
290
291
292	<cffunction name="QueryAppend" returntype="Query" output="false" access="private">
293		<cfargument name="QueryOne" type="Query"/>
294		<cfargument name="QueryTwo" type="Query"/>
295		<cfset var Result = -1/>
296		<!--- Bug fix for CF8 --->
297		<cfif NOT Arguments.QueryOne.RecordCount><cfreturn Arguments.QueryTwo /></cfif>
298		<cfif NOT Arguments.QueryTwo.RecordCount><cfreturn Arguments.QueryOne /></cfif>
299		<!--- / --->
300		<cfquery name="Result" dbtype="Query">
301			SELECT * FROM Arguments.QueryOne
302			UNION SELECT * FROM Arguments.QueryTwo
303		</cfquery>
304		<cfreturn Result/>
305	</cffunction>
306
307
308
309
310</cfcomponent>