Skip to content

Commit 5912194

Browse files
authored
feat(integrations): update sanitized inputs for github to read monorepo apps (#1929)
1 parent ec93c2e commit 5912194

File tree

4 files changed

+121
-40
lines changed

4 files changed

+121
-40
lines changed

apps/portal/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"dependencies": {
55
"@aws-sdk/client-s3": "^3.859.0",
66
"@aws-sdk/s3-request-presigner": "^3.859.0",
7+
"@hookform/resolvers": "^5.2.2",
78
"@prisma/client": "^6.13.0",
89
"@react-email/components": "^0.0.41",
910
"@react-email/render": "^1.1.2",
@@ -27,7 +28,9 @@
2728
"react": "^19.2.3",
2829
"react-dom": "^19.2.3",
2930
"react-email": "^4.0.15",
30-
"sonner": "^2.0.5"
31+
"react-hook-form": "^7.68.0",
32+
"sonner": "^2.0.5",
33+
"zod": "3"
3134
},
3235
"devDependencies": {
3336
"@tailwindcss/postcss": "^4.1.10",

bun.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@
306306
"dependencies": {
307307
"@aws-sdk/client-s3": "^3.859.0",
308308
"@aws-sdk/s3-request-presigner": "^3.859.0",
309+
"@hookform/resolvers": "^5.2.2",
309310
"@prisma/client": "^6.13.0",
310311
"@react-email/components": "^0.0.41",
311312
"@react-email/render": "^1.1.2",
@@ -329,7 +330,9 @@
329330
"react": "^19.2.3",
330331
"react-dom": "^19.2.3",
331332
"react-email": "^4.0.15",
333+
"react-hook-form": "^7.68.0",
332334
"sonner": "^2.0.5",
335+
"zod": "3",
333336
},
334337
"devDependencies": {
335338
"@tailwindcss/postcss": "^4.1.10",
@@ -5816,6 +5819,8 @@
58165819

58175820
"@comp/app/resend": ["[email protected]", "", { "dependencies": { "@react-email/render": "1.1.2" } }, "sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA=="],
58185821

5822+
"@comp/portal/zod": ["[email protected]", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
5823+
58195824
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
58205825

58215826
"@discordjs/rest/@discordjs/collection": ["@discordjs/[email protected]", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],

packages/integration-platform/src/manifests/github/checks/sanitized-inputs.ts

Lines changed: 92 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,35 @@
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

89
import { TASK_TEMPLATES } from '../../../task-mappings';
910
import 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';
1117
import { targetReposVariable } from '../variables';
1218

1319
const JS_VALIDATION_PACKAGES = ['zod'];
1420
const PY_VALIDATION_PACKAGES = ['pydantic'];
1521

22+
const TARGET_FILES = ['package.json', 'requirements.txt', 'pyproject.toml'];
23+
1624
interface 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+
2235
const 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+
3048
export 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
}

packages/integration-platform/src/manifests/github/types.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,23 @@ export interface GitHubCodeScanningDefaultSetup {
7575
query_suite?: 'default' | 'extended';
7676
updated_at?: string;
7777
}
78+
79+
/**
80+
* Git tree response
81+
* Returned by /repos/{owner}/{repo}/git/trees/{tree_sha}?recursive=1
82+
*/
83+
export interface GitHubTreeResponse {
84+
sha: string;
85+
url: string;
86+
truncated: boolean;
87+
tree: GitHubTreeEntry[];
88+
}
89+
90+
export interface GitHubTreeEntry {
91+
path: string;
92+
mode: string;
93+
type: 'blob' | 'tree';
94+
sha: string;
95+
size?: number;
96+
url: string;
97+
}

0 commit comments

Comments
 (0)