44 * Ensures repositories use a modern validation/sanitization library
55 * (Zod or Pydantic) and have automated static analysis (CodeQL) enabled.
66 * Supports monorepos by scanning all package.json/requirements.txt files.
7+ *
8+ * Code scanning detection supports:
9+ * - GitHub CodeQL default setup
10+ * - Custom CodeQL workflow files (.github/workflows/*.yml with codeql-action)
11+ * - Third-party SARIF uploaders
712 */
813
914import { TASK_TEMPLATES } from '../../../task-mappings' ;
@@ -21,6 +26,17 @@ const PY_VALIDATION_PACKAGES = ['pydantic'];
2126
2227const TARGET_FILES = [ 'package.json' , 'requirements.txt' , 'pyproject.toml' ] ;
2328
29+ // Patterns that indicate code scanning is configured in a workflow
30+ const CODE_SCANNING_PATTERNS = [
31+ 'github/codeql-action/init' ,
32+ 'github/codeql-action/analyze' ,
33+ 'github/codeql-action/upload-sarif' ,
34+ 'codeql-action/init' ,
35+ 'codeql-action/analyze' ,
36+ 'codeql-action/upload-sarif' ,
37+ 'upload-sarif' , // Generic SARIF upload
38+ ] ;
39+
2440interface GitHubFileResponse {
2541 content : string ;
2642 encoding : 'base64' | 'utf-8' ;
@@ -32,6 +48,17 @@ interface ValidationMatch {
3248 file : string ;
3349}
3450
51+ type CodeScanningStatus =
52+ | {
53+ status : 'enabled' ;
54+ method : 'default-setup' | 'workflow' ;
55+ languages ?: string [ ] ;
56+ workflow ?: string ;
57+ }
58+ | { status : 'not-configured' }
59+ | { status : 'permission-denied' ; isPrivate : boolean }
60+ | { status : 'ghas-required' } ;
61+
3562const decodeFile = ( file : GitHubFileResponse ) : string => {
3663 if ( ! file ?. content ) return '' ;
3764 if ( file . encoding === 'base64' ) {
@@ -160,18 +187,105 @@ export const sanitizedInputsCheck: IntegrationCheck = {
160187 return matches ;
161188 } ;
162189
163- const isCodeScanningEnabled = async ( repoName : string ) => {
190+ /**
191+ * Check if a workflow file contains code scanning configuration
192+ */
193+ const hasCodeScanningInWorkflow = ( content : string ) : boolean => {
194+ const lower = content . toLowerCase ( ) ;
195+ return CODE_SCANNING_PATTERNS . some ( ( pattern ) => lower . includes ( pattern . toLowerCase ( ) ) ) ;
196+ } ;
197+
198+ /**
199+ * Find workflow files that configure code scanning
200+ */
201+ const findCodeScanningWorkflows = async (
202+ repoName : string ,
203+ tree : GitHubTreeEntry [ ] ,
204+ ) : Promise < string [ ] > => {
205+ const workflowFiles = tree . filter (
206+ ( entry ) =>
207+ entry . type === 'blob' &&
208+ entry . path . startsWith ( '.github/workflows/' ) &&
209+ ( entry . path . endsWith ( '.yml' ) || entry . path . endsWith ( '.yaml' ) ) ,
210+ ) ;
211+
212+ const codeScanningWorkflows : string [ ] = [ ] ;
213+
214+ for ( const entry of workflowFiles ) {
215+ const content = await fetchFile ( repoName , entry . path ) ;
216+ if ( content && hasCodeScanningInWorkflow ( content ) ) {
217+ codeScanningWorkflows . push ( entry . path ) ;
218+ }
219+ }
220+
221+ return codeScanningWorkflows ;
222+ } ;
223+
224+ /**
225+ * Check code scanning status using multiple detection methods
226+ */
227+ const getCodeScanningStatus = async ( {
228+ repoName,
229+ tree,
230+ isPrivate,
231+ isGhasEnabled,
232+ } : {
233+ repoName : string ;
234+ tree : GitHubTreeEntry [ ] ;
235+ isPrivate : boolean ;
236+ isGhasEnabled : boolean ;
237+ } ) : Promise < CodeScanningStatus > => {
238+ // First, try the default setup API
164239 try {
165240 const setup = await ctx . fetch < GitHubCodeScanningDefaultSetup > (
166241 `/repos/${ repoName } /code-scanning/default-setup` ,
167242 ) ;
168- return setup . state === 'configured'
169- ? { enabled : true , languages : setup . languages || [ ] }
170- : { enabled : false , languages : setup . languages || [ ] } ;
243+ if ( setup . state === 'configured' ) {
244+ return {
245+ status : 'enabled' ,
246+ method : 'default-setup' ,
247+ languages : setup . languages || [ ] ,
248+ } ;
249+ }
171250 } catch ( error ) {
172- ctx . log ( `Code scanning not configured for ${ repoName } : ${ String ( error ) } ` ) ;
173- return { enabled : false } ;
251+ const errorStr = String ( error ) ;
252+
253+ // Check for 403 Forbidden - indicates permission issues or GHAS not enabled
254+ // Return early without checking workflows since we can't verify they're actually running
255+ if ( errorStr . includes ( '403' ) || errorStr . includes ( 'Forbidden' ) ) {
256+ ctx . log (
257+ `Code scanning API returned 403 for ${ repoName } (private: ${ isPrivate } , ghas: ${ isGhasEnabled } )` ,
258+ ) ;
259+
260+ // For private repos, distinguish between GHAS not enabled vs permission issue
261+ if ( isPrivate ) {
262+ if ( isGhasEnabled ) {
263+ // GHAS is enabled but we still got 403 - it's a permission issue
264+ return { status : 'permission-denied' , isPrivate } ;
265+ }
266+ // GHAS is not enabled - that's why we got 403
267+ return { status : 'ghas-required' } ;
268+ }
269+
270+ return { status : 'permission-denied' , isPrivate } ;
271+ }
272+
273+ // Other errors - API might not be available, continue to check workflows
274+ ctx . log ( `Code scanning default setup not available for ${ repoName } : ${ errorStr } ` ) ;
174275 }
276+
277+ // Fall back to checking for workflow files with code scanning
278+ // Only reached if API didn't return 403 (meaning we have access but default setup isn't on)
279+ const codeScanningWorkflows = await findCodeScanningWorkflows ( repoName , tree ) ;
280+ if ( codeScanningWorkflows . length > 0 ) {
281+ return {
282+ status : 'enabled' ,
283+ method : 'workflow' ,
284+ workflow : codeScanningWorkflows [ 0 ] ,
285+ } ;
286+ }
287+
288+ return { status : 'not-configured' } ;
175289 } ;
176290
177291 for ( const repoName of targetRepos ) {
@@ -181,8 +295,18 @@ export const sanitizedInputsCheck: IntegrationCheck = {
181295 // Fetch the full tree to find all package.json/requirements.txt files
182296 const tree = await fetchRepoTree ( repo . full_name , repo . default_branch ) ;
183297 const validationMatches = await findValidationLibraries ( repo . full_name , tree ) ;
184- const codeScanning = await isCodeScanningEnabled ( repo . full_name ) ;
185298
299+ // Check if GHAS is enabled (for private repos, this helps distinguish permission issues)
300+ const isGhasEnabled = repo . security_and_analysis ?. advanced_security ?. status === 'enabled' ;
301+
302+ const codeScanningStatus = await getCodeScanningStatus ( {
303+ repoName : repo . full_name ,
304+ tree,
305+ isPrivate : repo . private ,
306+ isGhasEnabled,
307+ } ) ;
308+
309+ // Report input validation results
186310 if ( validationMatches . length > 0 ) {
187311 ctx . pass ( {
188312 title : `Input validation enabled in ${ repo . name } ` ,
@@ -216,30 +340,69 @@ export const sanitizedInputsCheck: IntegrationCheck = {
216340 } ) ;
217341 }
218342
219- if ( codeScanning . enabled ) {
220- ctx . pass ( {
221- title : `CodeQL scanning configured for ${ repo . name } ` ,
222- description : 'GitHub CodeQL default setup is enabled.' ,
223- resourceType : 'repository' ,
224- resourceId : repo . full_name ,
225- evidence : {
226- repository : repo . full_name ,
227- codeScanning : 'CodeQL default setup' ,
228- languages : codeScanning . languages ,
229- checkedAt : new Date ( ) . toISOString ( ) ,
230- } ,
231- } ) ;
232- } else {
233- ctx . fail ( {
234- title : `Code scanning not enabled for ${ repo . name } ` ,
235- description :
236- 'GitHub CodeQL (or an equivalent static analysis tool) is not configured. Enable it to automatically detect insecure patterns.' ,
237- resourceType : 'repository' ,
238- resourceId : repo . full_name ,
239- severity : 'medium' ,
240- remediation :
241- 'In the repository Security tab, enable CodeQL default setup (or add a custom workflow) to run on every push.' ,
242- } ) ;
343+ // Report code scanning results based on status
344+ switch ( codeScanningStatus . status ) {
345+ case 'enabled' : {
346+ const methodDescription =
347+ codeScanningStatus . method === 'default-setup'
348+ ? 'GitHub CodeQL default setup is enabled.'
349+ : `Code scanning configured via workflow: ${ codeScanningStatus . workflow } ` ;
350+
351+ ctx . pass ( {
352+ title : `CodeQL scanning configured for ${ repo . name } ` ,
353+ description : methodDescription ,
354+ resourceType : 'repository' ,
355+ resourceId : repo . full_name ,
356+ evidence : {
357+ repository : repo . full_name ,
358+ codeScanning : codeScanningStatus . method ,
359+ ...( codeScanningStatus . languages && { languages : codeScanningStatus . languages } ) ,
360+ ...( codeScanningStatus . workflow && { workflow : codeScanningStatus . workflow } ) ,
361+ checkedAt : new Date ( ) . toISOString ( ) ,
362+ } ,
363+ } ) ;
364+ break ;
365+ }
366+
367+ case 'ghas-required' :
368+ ctx . fail ( {
369+ title : `Code scanning requires GitHub Advanced Security for ${ repo . name } ` ,
370+ description :
371+ 'This is a private repository. GitHub Advanced Security (GHAS) must be enabled before CodeQL can be configured. GHAS is a paid feature for private repositories.' ,
372+ resourceType : 'repository' ,
373+ resourceId : repo . full_name ,
374+ severity : 'medium' ,
375+ remediation :
376+ 'Enable GitHub Advanced Security in the repository settings (Settings → Code security and analysis → GitHub Advanced Security), then enable CodeQL.' ,
377+ } ) ;
378+ break ;
379+
380+ case 'permission-denied' :
381+ ctx . fail ( {
382+ title : `Cannot access code scanning configuration for ${ repo . name } ` ,
383+ description :
384+ 'The GitHub integration does not have permission to read code scanning configuration. This may be due to missing permissions or organization policies.' ,
385+ resourceType : 'repository' ,
386+ resourceId : repo . full_name ,
387+ severity : 'medium' ,
388+ remediation :
389+ 'Ensure the GitHub App has "Code scanning alerts: Read" permission. If this is an organization repository, check that organization policies allow access.' ,
390+ } ) ;
391+ break ;
392+
393+ case 'not-configured' :
394+ default :
395+ ctx . fail ( {
396+ title : `Code scanning not enabled for ${ repo . name } ` ,
397+ description :
398+ 'GitHub CodeQL (or an equivalent static analysis tool) is not configured. Enable it to automatically detect insecure patterns.' ,
399+ resourceType : 'repository' ,
400+ resourceId : repo . full_name ,
401+ severity : 'medium' ,
402+ remediation :
403+ 'In the repository Security tab, enable CodeQL default setup (or add a custom workflow) to run on every push.' ,
404+ } ) ;
405+ break ;
243406 }
244407 }
245408 } ,
0 commit comments