Skip to content

Commit 413088b

Browse files
authored
Merge pull request #2683 from trycompai/feat/split-sanitized-inputs-check
feat(integrations): split GitHub sanitized inputs check into two automations
2 parents 9cd78d1 + 07739f8 commit 413088b

4 files changed

Lines changed: 338 additions & 260 deletions

File tree

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
/**
2+
* Code Scanning Check
3+
*
4+
* Verifies repositories have automated static analysis configured. Detects:
5+
* - GitHub CodeQL default setup
6+
* - Custom CodeQL workflow files (.github/workflows/*.yml with codeql-action)
7+
* - Third-party SARIF uploaders (Semgrep, Snyk, Trivy, etc.)
8+
*/
9+
10+
import { TASK_TEMPLATES } from '../../../task-mappings';
11+
import type { IntegrationCheck } from '../../../types';
12+
import type {
13+
GitHubCodeScanningDefaultSetup,
14+
GitHubRepo,
15+
GitHubTreeEntry,
16+
GitHubTreeResponse,
17+
} from '../types';
18+
import { parseRepoBranch, targetReposVariable } from '../variables';
19+
20+
// Patterns that indicate code scanning is configured in a workflow
21+
const CODE_SCANNING_PATTERNS = [
22+
'github/codeql-action/init',
23+
'github/codeql-action/analyze',
24+
'github/codeql-action/upload-sarif',
25+
'codeql-action/init',
26+
'codeql-action/analyze',
27+
'codeql-action/upload-sarif',
28+
'upload-sarif', // Generic SARIF upload
29+
];
30+
31+
interface GitHubFileResponse {
32+
content: string;
33+
encoding: 'base64' | 'utf-8';
34+
path: string;
35+
}
36+
37+
type CodeScanningStatus =
38+
| {
39+
status: 'enabled';
40+
method: 'default-setup' | 'workflow';
41+
languages?: string[];
42+
workflow?: string;
43+
}
44+
| { status: 'not-configured' }
45+
| { status: 'permission-denied'; isPrivate: boolean }
46+
| { status: 'ghas-required' };
47+
48+
const decodeFile = (file: GitHubFileResponse): string => {
49+
if (!file?.content) return '';
50+
if (file.encoding === 'base64') {
51+
return Buffer.from(file.content, 'base64').toString('utf-8');
52+
}
53+
return file.content;
54+
};
55+
56+
export const codeScanningCheck: IntegrationCheck = {
57+
id: 'code_scanning',
58+
name: 'Code Scanning',
59+
description:
60+
'Verifies repositories have GitHub CodeQL or an equivalent static analysis tool configured. Detects default-setup CodeQL, custom CodeQL workflows, and third-party SARIF uploaders.',
61+
service: 'code-security',
62+
taskMapping: TASK_TEMPLATES.sanitizedInputs,
63+
defaultSeverity: 'medium',
64+
variables: [targetReposVariable],
65+
66+
run: async (ctx) => {
67+
const targetReposRaw = (ctx.variables.target_repos as string[] | undefined) ?? [];
68+
const targetRepos = targetReposRaw.map((v) => parseRepoBranch(v).repo);
69+
70+
if (targetRepos.length === 0) {
71+
ctx.fail({
72+
title: 'No repositories selected',
73+
description:
74+
'Select at least one repository to monitor in the integration settings so we can verify code scanning.',
75+
resourceType: 'integration',
76+
resourceId: 'github',
77+
severity: 'low',
78+
remediation: 'Open the integration settings and choose repositories to monitor.',
79+
});
80+
return;
81+
}
82+
83+
const fetchRepo = async (fullName: string): Promise<GitHubRepo | null> => {
84+
try {
85+
return await ctx.fetch<GitHubRepo>(`/repos/${fullName}`);
86+
} catch (error) {
87+
ctx.warn(`Failed to fetch repo ${fullName}: ${String(error)}`);
88+
return null;
89+
}
90+
};
91+
92+
const fetchRepoTree = async (repoName: string, branch: string): Promise<GitHubTreeEntry[]> => {
93+
try {
94+
const tree = await ctx.fetch<GitHubTreeResponse>(
95+
`/repos/${repoName}/git/trees/${branch}?recursive=1`,
96+
);
97+
if (tree.truncated) {
98+
ctx.warn(`Repository ${repoName} has too many files, tree was truncated`);
99+
}
100+
return tree.tree;
101+
} catch (error) {
102+
ctx.warn(`Failed to fetch tree for ${repoName}: ${String(error)}`);
103+
return [];
104+
}
105+
};
106+
107+
const fetchFile = async (repoName: string, path: string): Promise<string | null> => {
108+
try {
109+
const file = await ctx.fetch<GitHubFileResponse>(`/repos/${repoName}/contents/${path}`);
110+
return decodeFile(file);
111+
} catch {
112+
return null;
113+
}
114+
};
115+
116+
const hasCodeScanningInWorkflow = (content: string): boolean => {
117+
const lower = content.toLowerCase();
118+
return CODE_SCANNING_PATTERNS.some((pattern) => lower.includes(pattern.toLowerCase()));
119+
};
120+
121+
const findCodeScanningWorkflows = async (
122+
repoName: string,
123+
tree: GitHubTreeEntry[],
124+
): Promise<string[]> => {
125+
const workflowFiles = tree.filter(
126+
(entry) =>
127+
entry.type === 'blob' &&
128+
entry.path.startsWith('.github/workflows/') &&
129+
(entry.path.endsWith('.yml') || entry.path.endsWith('.yaml')),
130+
);
131+
132+
const codeScanningWorkflows: string[] = [];
133+
134+
for (const entry of workflowFiles) {
135+
const content = await fetchFile(repoName, entry.path);
136+
if (content && hasCodeScanningInWorkflow(content)) {
137+
codeScanningWorkflows.push(entry.path);
138+
}
139+
}
140+
141+
return codeScanningWorkflows;
142+
};
143+
144+
const getCodeScanningStatus = async ({
145+
repoName,
146+
tree,
147+
isPrivate,
148+
isGhasEnabled,
149+
}: {
150+
repoName: string;
151+
tree: GitHubTreeEntry[];
152+
isPrivate: boolean;
153+
isGhasEnabled: boolean;
154+
}): Promise<CodeScanningStatus> => {
155+
let apiGot403 = false;
156+
157+
// First, try the default setup API
158+
try {
159+
const setup = await ctx.fetch<GitHubCodeScanningDefaultSetup>(
160+
`/repos/${repoName}/code-scanning/default-setup`,
161+
);
162+
if (setup.state === 'configured') {
163+
return {
164+
status: 'enabled',
165+
method: 'default-setup',
166+
languages: setup.languages || [],
167+
};
168+
}
169+
} catch (error) {
170+
const errorStr = String(error);
171+
172+
if (errorStr.includes('403') || errorStr.includes('Forbidden')) {
173+
// The code-scanning API requires GHAS for private repos, but reading
174+
// workflow file contents only requires contents:read. A 403 here does
175+
// not mean we can't check for code scanning workflows.
176+
ctx.log(
177+
`Code scanning API returned 403 for ${repoName} (private: ${isPrivate}, ghas: ${isGhasEnabled}). Falling back to workflow file scanning.`,
178+
);
179+
apiGot403 = true;
180+
} else {
181+
// Other errors - API might not be available, continue to check workflows
182+
ctx.log(`Code scanning default setup not available for ${repoName}: ${errorStr}`);
183+
}
184+
}
185+
186+
// Fall back to checking for workflow files with code scanning.
187+
// This catches repos using third-party SAST tools (Semgrep, Snyk, Trivy, etc.)
188+
// that upload SARIF results via github/codeql-action/upload-sarif.
189+
const codeScanningWorkflows = await findCodeScanningWorkflows(repoName, tree);
190+
if (codeScanningWorkflows.length > 0) {
191+
return {
192+
status: 'enabled',
193+
method: 'workflow',
194+
workflow: codeScanningWorkflows[0],
195+
};
196+
}
197+
198+
if (apiGot403) {
199+
if (isPrivate) {
200+
if (isGhasEnabled) {
201+
return { status: 'permission-denied', isPrivate };
202+
}
203+
return { status: 'ghas-required' };
204+
}
205+
return { status: 'permission-denied', isPrivate };
206+
}
207+
208+
return { status: 'not-configured' };
209+
};
210+
211+
for (const repoName of targetRepos) {
212+
const repo = await fetchRepo(repoName);
213+
if (!repo) continue;
214+
215+
const tree = await fetchRepoTree(repo.full_name, repo.default_branch);
216+
const isGhasEnabled = repo.security_and_analysis?.advanced_security?.status === 'enabled';
217+
218+
const codeScanningStatus = await getCodeScanningStatus({
219+
repoName: repo.full_name,
220+
tree,
221+
isPrivate: repo.private,
222+
isGhasEnabled,
223+
});
224+
225+
switch (codeScanningStatus.status) {
226+
case 'enabled': {
227+
const methodDescription =
228+
codeScanningStatus.method === 'default-setup'
229+
? 'GitHub CodeQL default setup is enabled.'
230+
: `Code scanning configured via workflow: ${codeScanningStatus.workflow}`;
231+
232+
ctx.pass({
233+
title: `CodeQL scanning configured for ${repo.name}`,
234+
description: methodDescription,
235+
resourceType: 'repository',
236+
resourceId: repo.full_name,
237+
evidence: {
238+
[repo.full_name]: {
239+
code_scanning: {
240+
status: 'enabled',
241+
method: codeScanningStatus.method,
242+
...(codeScanningStatus.languages && { languages: codeScanningStatus.languages }),
243+
...(codeScanningStatus.workflow && { workflow: codeScanningStatus.workflow }),
244+
checked_at: new Date().toISOString(),
245+
},
246+
},
247+
},
248+
});
249+
break;
250+
}
251+
252+
case 'ghas-required':
253+
ctx.fail({
254+
title: `Code scanning requires GitHub Advanced Security for ${repo.name}`,
255+
description:
256+
'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.',
257+
resourceType: 'repository',
258+
resourceId: repo.full_name,
259+
severity: 'medium',
260+
remediation:
261+
'Enable GitHub Advanced Security in the repository settings (Settings → Code security and analysis → GitHub Advanced Security), then enable CodeQL.',
262+
evidence: {
263+
[repo.full_name]: {
264+
code_scanning: {
265+
status: 'ghas_required',
266+
checked_at: new Date().toISOString(),
267+
},
268+
},
269+
},
270+
});
271+
break;
272+
273+
case 'permission-denied':
274+
ctx.fail({
275+
title: `Cannot access code scanning configuration for ${repo.name}`,
276+
description:
277+
'The GitHub integration does not have permission to read code scanning configuration. This may be due to missing permissions or organization policies.',
278+
resourceType: 'repository',
279+
resourceId: repo.full_name,
280+
severity: 'medium',
281+
remediation:
282+
'Ensure the GitHub App has "Code scanning alerts: Read" permission. If this is an organization repository, check that organization policies allow access.',
283+
evidence: {
284+
[repo.full_name]: {
285+
code_scanning: {
286+
status: 'permission_denied',
287+
checked_at: new Date().toISOString(),
288+
},
289+
},
290+
},
291+
});
292+
break;
293+
294+
case 'not-configured':
295+
default:
296+
ctx.fail({
297+
title: `Code scanning not enabled for ${repo.name}`,
298+
description:
299+
'GitHub CodeQL (or an equivalent static analysis tool) is not configured. Enable it to automatically detect insecure patterns.',
300+
resourceType: 'repository',
301+
resourceId: repo.full_name,
302+
severity: 'medium',
303+
remediation:
304+
'In the repository Security tab, enable CodeQL default setup (or add a custom workflow) to run on every push.',
305+
evidence: {
306+
[repo.full_name]: {
307+
code_scanning: {
308+
status: 'not_configured',
309+
checked_at: new Date().toISOString(),
310+
},
311+
},
312+
},
313+
});
314+
break;
315+
}
316+
}
317+
},
318+
};

packages/integration-platform/src/manifests/github/checks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
export { branchProtectionCheck } from './branch-protection';
7+
export { codeScanningCheck } from './code-scanning';
78
export { dependabotCheck } from './dependabot';
89
export { sanitizedInputsCheck } from './sanitized-inputs';
910
export { twoFactorAuthCheck } from './two-factor-auth';

0 commit comments

Comments
 (0)