33 *
44 * Ensures repositories use a modern validation/sanitization library
55 * (Zod or Pydantic) and have automated static analysis (CodeQL) enabled.
6+ * Supports monorepos by scanning all package.json/requirements.txt files.
67 */
78
89import { TASK_TEMPLATES } from '../../../task-mappings' ;
910import type { IntegrationCheck } from '../../../types' ;
10- import type { GitHubCodeScanningDefaultSetup , GitHubRepo } from '../types' ;
11+ import type {
12+ GitHubCodeScanningDefaultSetup ,
13+ GitHubRepo ,
14+ GitHubTreeEntry ,
15+ GitHubTreeResponse ,
16+ } from '../types' ;
1117import { targetReposVariable } from '../variables' ;
1218
1319const JS_VALIDATION_PACKAGES = [ 'zod' ] ;
1420const PY_VALIDATION_PACKAGES = [ 'pydantic' ] ;
1521
22+ const TARGET_FILES = [ 'package.json' , 'requirements.txt' , 'pyproject.toml' ] ;
23+
1624interface GitHubFileResponse {
1725 content : string ;
1826 encoding : 'base64' | 'utf-8' ;
1927 path : string ;
2028}
2129
30+ interface ValidationMatch {
31+ library : string ;
32+ file : string ;
33+ }
34+
2235const decodeFile = ( file : GitHubFileResponse ) : string => {
2336 if ( ! file ?. content ) return '' ;
2437 if ( file . encoding === 'base64' ) {
@@ -27,11 +40,16 @@ const decodeFile = (file: GitHubFileResponse): string => {
2740 return file . content ;
2841} ;
2942
43+ const getFileName = ( path : string ) : string => {
44+ const parts = path . split ( '/' ) ;
45+ return parts [ parts . length - 1 ] ?? path ;
46+ } ;
47+
3048export const sanitizedInputsCheck : IntegrationCheck = {
3149 id : 'sanitized_inputs' ,
3250 name : 'Sanitized Inputs & Code Scanning' ,
3351 description :
34- 'Verifies repositories use Zod/Pydantic for input validation and have GitHub CodeQL scanning enabled.' ,
52+ 'Verifies repositories use Zod/Pydantic for input validation and have GitHub CodeQL scanning enabled. Scans entire repository including monorepo subdirectories. ' ,
3553 taskMapping : TASK_TEMPLATES . sanitizedInputs ,
3654 defaultSeverity : 'medium' ,
3755 variables : [ targetReposVariable ] ,
@@ -61,6 +79,21 @@ export const sanitizedInputsCheck: IntegrationCheck = {
6179 }
6280 } ;
6381
82+ const fetchRepoTree = async ( repoName : string , branch : string ) : Promise < GitHubTreeEntry [ ] > => {
83+ try {
84+ const tree = await ctx . fetch < GitHubTreeResponse > (
85+ `/repos/${ repoName } /git/trees/${ branch } ?recursive=1` ,
86+ ) ;
87+ if ( tree . truncated ) {
88+ ctx . warn ( `Repository ${ repoName } has too many files, tree was truncated` ) ;
89+ }
90+ return tree . tree ;
91+ } catch ( error ) {
92+ ctx . warn ( `Failed to fetch tree for ${ repoName } : ${ String ( error ) } ` ) ;
93+ return [ ] ;
94+ }
95+ } ;
96+
6497 const fetchFile = async ( repoName : string , path : string ) : Promise < string | null > => {
6598 try {
6699 const file = await ctx . fetch < GitHubFileResponse > ( `/repos/${ repoName } /contents/${ path } ` ) ;
@@ -70,46 +103,61 @@ export const sanitizedInputsCheck: IntegrationCheck = {
70103 }
71104 } ;
72105
73- const hasValidationLibrary = async ( repoName : string ) => {
74- // Check package.json for JS libraries
75- const packageJsonRaw = await fetchFile ( repoName , 'package.json' ) ;
76- if ( packageJsonRaw ) {
77- try {
78- const pkg = JSON . parse ( packageJsonRaw ) ;
79- const deps = {
80- ...( pkg . dependencies || { } ) ,
81- ...( pkg . devDependencies || { } ) ,
82- } ;
83- for ( const candidate of JS_VALIDATION_PACKAGES ) {
84- if ( deps [ candidate ] ) {
85- return { found : true , library : candidate , file : 'package.json' } ;
86- }
106+ const checkPackageJson = ( content : string , filePath : string ) : ValidationMatch | null => {
107+ try {
108+ const pkg = JSON . parse ( content ) ;
109+ const deps = {
110+ ...( pkg . dependencies || { } ) ,
111+ ...( pkg . devDependencies || { } ) ,
112+ } ;
113+ for ( const candidate of JS_VALIDATION_PACKAGES ) {
114+ if ( deps [ candidate ] ) {
115+ return { library : candidate , file : filePath } ;
87116 }
88- } catch {
89- ctx . warn ( `Unable to parse package.json for ${ repoName } ` ) ;
90117 }
118+ } catch {
119+ // Invalid JSON, skip
91120 }
121+ return null ;
122+ } ;
92123
93- // Check requirements.txt or pyproject.toml for Python libraries
94- const requirementsRaw = await fetchFile ( repoName , 'requirements.txt' ) ;
95- if ( requirementsRaw ) {
96- const lower = requirementsRaw . toLowerCase ( ) ;
97- const candidate = PY_VALIDATION_PACKAGES . find ( ( pkg ) => lower . includes ( pkg ) ) ;
98- if ( candidate ) {
99- return { found : true , library : candidate , file : 'requirements.txt' } ;
124+ const checkPythonFile = ( content : string , filePath : string ) : ValidationMatch | null => {
125+ const lower = content . toLowerCase ( ) ;
126+ for ( const candidate of PY_VALIDATION_PACKAGES ) {
127+ if ( lower . includes ( candidate ) ) {
128+ return { library : candidate , file : filePath } ;
100129 }
101130 }
131+ return null ;
132+ } ;
102133
103- const pyprojectRaw = await fetchFile ( repoName , 'pyproject.toml' ) ;
104- if ( pyprojectRaw ) {
105- const lower = pyprojectRaw . toLowerCase ( ) ;
106- const candidate = PY_VALIDATION_PACKAGES . find ( ( pkg ) => lower . includes ( pkg ) ) ;
107- if ( candidate ) {
108- return { found : true , library : candidate , file : 'pyproject.toml' } ;
134+ const findValidationLibraries = async (
135+ repoName : string ,
136+ tree : GitHubTreeEntry [ ] ,
137+ ) : Promise < ValidationMatch [ ] > => {
138+ const matches : ValidationMatch [ ] = [ ] ;
139+
140+ // Find all target files in the tree
141+ const targetEntries = tree . filter (
142+ ( entry ) => entry . type === 'blob' && TARGET_FILES . includes ( getFileName ( entry . path ) ) ,
143+ ) ;
144+
145+ for ( const entry of targetEntries ) {
146+ const content = await fetchFile ( repoName , entry . path ) ;
147+ if ( ! content ) continue ;
148+
149+ const fileName = getFileName ( entry . path ) ;
150+
151+ if ( fileName === 'package.json' ) {
152+ const match = checkPackageJson ( content , entry . path ) ;
153+ if ( match ) matches . push ( match ) ;
154+ } else if ( fileName === 'requirements.txt' || fileName === 'pyproject.toml' ) {
155+ const match = checkPythonFile ( content , entry . path ) ;
156+ if ( match ) matches . push ( match ) ;
109157 }
110158 }
111159
112- return { found : false } ;
160+ return matches ;
113161 } ;
114162
115163 const isCodeScanningEnabled = async ( repoName : string ) => {
@@ -130,35 +178,40 @@ export const sanitizedInputsCheck: IntegrationCheck = {
130178 const repo = await fetchRepo ( repoName ) ;
131179 if ( ! repo ) continue ;
132180
133- const validation = await hasValidationLibrary ( repo . full_name ) ;
181+ // Fetch the full tree to find all package.json/requirements.txt files
182+ const tree = await fetchRepoTree ( repo . full_name , repo . default_branch ) ;
183+ const validationMatches = await findValidationLibraries ( repo . full_name , tree ) ;
134184 const codeScanning = await isCodeScanningEnabled ( repo . full_name ) ;
135185
136- if ( validation . found ) {
186+ if ( validationMatches . length > 0 ) {
137187 ctx . pass ( {
138188 title : `Input validation enabled in ${ repo . name } ` ,
139- description : `Detected ${ validation . library } usage (${ validation . file } ).` ,
189+ description : `Found ${ validationMatches . length } location(s) with validation libraries: ${ validationMatches . map ( ( m ) => ` ${ m . library } (${ m . file } )` ) . join ( ', ' ) } .` ,
140190 resourceType : 'repository' ,
141191 resourceId : repo . full_name ,
142192 evidence : {
143193 repository : repo . full_name ,
144- library : validation . library ,
145- file : validation . file ,
194+ matches : validationMatches ,
146195 checkedAt : new Date ( ) . toISOString ( ) ,
147196 } ,
148197 } ) ;
149198 } else {
199+ const checkedFiles = tree
200+ . filter ( ( e ) => e . type === 'blob' && TARGET_FILES . includes ( getFileName ( e . path ) ) )
201+ . map ( ( e ) => e . path ) ;
202+
150203 ctx . fail ( {
151204 title : `No input validation library found in ${ repo . name } ` ,
152205 description :
153- 'Could not detect Zod or Pydantic. Implement input validation and sanitization using one of these libraries.' ,
206+ 'Could not detect Zod or Pydantic in any package.json, requirements.txt, or pyproject.toml . Implement input validation and sanitization using one of these libraries.' ,
154207 resourceType : 'repository' ,
155208 resourceId : repo . full_name ,
156209 severity : 'medium' ,
157210 remediation :
158211 'Add Zod (JavaScript/TypeScript) or Pydantic (Python) to enforce schema validation on inbound data.' ,
159212 evidence : {
160213 repository : repo . full_name ,
161- checkedFiles : [ 'package.json' , 'requirements.txt' , 'pyproject.toml '] ,
214+ checkedFiles : checkedFiles . length > 0 ? checkedFiles : [ 'No dependency files found '] ,
162215 } ,
163216 } ) ;
164217 }
0 commit comments