Skip to content

Commit 6c49228

Browse files
tofikwestclaude
andcommitted
fix(github): scope 2FA check to selected repos' orgs only
The 2FA check was iterating every org returned by /user/orgs, which included orgs the connected user happened to belong to but the customer never selected. eighteenlabs saw findings for sisoputnfrba and dds-utn (personal orgs of the connecting account) alongside their own org. Derive the orgs to check from ctx.variables.target_repos instead — the same selection the user already configures in the integration UI. Drop the /user/orgs call entirely. The user-selected list is already filtered to Organization-owned repos by targetReposVariable.fetchOptions. Reverts the silent 422 skip from b5f9f3d: now that the org list comes from explicit user selection, a 422 means the customer selected a repo in an org they don't own — that's a real misconfiguration and should surface as a finding. Fixes CS-259 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b5f9f3d commit 6c49228

File tree

1 file changed

+64
-57
lines changed

1 file changed

+64
-57
lines changed

packages/integration-platform/src/manifests/github/checks/two-factor-auth.ts

Lines changed: 64 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import { TASK_TEMPLATES } from '../../../task-mappings';
1212
import type { IntegrationCheck } from '../../../types';
13-
import type { GitHubOrg } from '../types';
13+
import { parseRepoBranches, targetReposVariable } from '../variables';
1414

1515
interface GitHubOrgMember {
1616
login: string;
@@ -81,46 +81,43 @@ export const twoFactorAuthCheck: IntegrationCheck = {
8181
taskMapping: TASK_TEMPLATES.twoFactorAuth,
8282
defaultSeverity: 'high',
8383

84-
run: async (ctx) => {
85-
// Step 1: Get all orgs the authenticated user belongs to
86-
let orgs: GitHubOrg[];
87-
try {
88-
orgs = await ctx.fetchAllPages<GitHubOrg>('/user/orgs');
89-
} catch (error) {
90-
const errorMsg = error instanceof Error ? error.message : String(error);
91-
ctx.error(`Failed to fetch organizations: ${errorMsg}`);
92-
ctx.fail({
93-
title: 'Cannot fetch GitHub organizations',
94-
description: `Failed to list organizations: ${errorMsg}`,
95-
resourceType: 'organization',
96-
resourceId: 'github',
97-
severity: 'medium',
98-
remediation:
99-
'Ensure the GitHub integration has the read:org scope. You may need to reconnect the integration.',
100-
});
101-
return;
102-
}
84+
variables: [targetReposVariable],
10385

104-
if (orgs.length === 0) {
86+
run: async (ctx) => {
87+
// Derive the orgs to check from the user-selected repositories.
88+
// We intentionally do NOT call /user/orgs — checking orgs the user happens to
89+
// belong to but did not select would surface findings for unrelated orgs
90+
// (e.g. personal side-project orgs) and confuse customers.
91+
const targetRepos = ctx.variables.target_repos as string[] | undefined;
92+
const orgsToCheck = Array.from(
93+
new Set(
94+
(targetRepos ?? [])
95+
.map((value) => parseRepoBranches(value).repo.split('/')[0])
96+
.filter((owner): owner is string => Boolean(owner)),
97+
),
98+
);
99+
100+
if (orgsToCheck.length === 0) {
105101
ctx.fail({
106-
title: 'No GitHub organizations found',
102+
title: 'No repositories configured',
107103
description:
108-
'The connected GitHub account is not a member of any organizations. 2FA enforcement is an organization-level setting.',
109-
resourceType: 'organization',
104+
'No repositories are configured for 2FA enforcement checking. Please select at least one repository.',
105+
resourceType: 'integration',
110106
resourceId: 'github',
111107
severity: 'low',
112-
remediation:
113-
'Connect a GitHub account that belongs to at least one organization.',
108+
remediation: 'Open the integration settings and select repositories to monitor.',
114109
});
115110
return;
116111
}
117112

118-
ctx.log(`Found ${orgs.length} organization(s). Checking 2FA status...`);
113+
ctx.log(
114+
`Checking 2FA for ${orgsToCheck.length} organization(s) derived from selected repos: ${orgsToCheck.join(', ')}`,
115+
);
119116

120117
// Step 2: For each org, check for members without 2FA
121-
for (const org of orgs) {
122-
ctx.log(`Checking 2FA for organization: ${org.login}`);
123-
const orgSlug = encodeURIComponent(org.login);
118+
for (const orgLogin of orgsToCheck) {
119+
ctx.log(`Checking 2FA for organization: ${orgLogin}`);
120+
const orgSlug = encodeURIComponent(orgLogin);
124121
const checkedAt = new Date().toISOString();
125122

126123
let membersWithout2FA: GitHubOrgMember[];
@@ -132,13 +129,13 @@ export const twoFactorAuthCheck: IntegrationCheck = {
132129
const errorMsg = error instanceof Error ? error.message : String(error);
133130

134131
if (isSamlSsoError(errorMsg)) {
135-
ctx.warn(`Cannot check 2FA for ${org.login}: SSO authorization is required.`);
132+
ctx.warn(`Cannot check 2FA for ${orgLogin}: SSO authorization is required.`);
136133
ctx.fail({
137-
title: `Cannot verify 2FA for ${org.login}`,
134+
title: `Cannot verify 2FA for ${orgLogin}`,
138135
description:
139136
'GitHub organization SSO authorization is required to access organization members.',
140137
resourceType: 'organization',
141-
resourceId: org.login,
138+
resourceId: orgLogin,
142139
severity: 'medium',
143140
remediation:
144141
'Authorize this OAuth app for your organization SSO, then rerun the check.',
@@ -147,35 +144,45 @@ export const twoFactorAuthCheck: IntegrationCheck = {
147144
}
148145

149146
if (isRateLimitError(error, errorMsg)) {
150-
ctx.warn(`Rate limit reached while checking 2FA for ${org.login}.`);
147+
ctx.warn(`Rate limit reached while checking 2FA for ${orgLogin}.`);
151148
ctx.fail({
152-
title: `Rate limited while checking ${org.login}`,
149+
title: `Rate limited while checking ${orgLogin}`,
153150
description:
154151
'GitHub rate limits prevented completion of this 2FA check for the organization.',
155152
resourceType: 'organization',
156-
resourceId: org.login,
153+
resourceId: orgLogin,
157154
severity: 'low',
158155
remediation: 'Wait for the GitHub rate limit to reset, then rerun the check.',
159156
});
160157
continue;
161158
}
162159

163-
// GitHub returns 422 when the caller is not an org owner for 2fa_* filters.
164-
// Silently skip these orgs — the connected user belongs to them but isn't an owner,
165-
// so we can't check 2FA and shouldn't surface a noisy finding for an unrelated org.
160+
// The user explicitly selected a repo in this org but isn't an owner.
161+
// Surface as a finding so they know to either reconnect with an owner
162+
// account or remove the repo from the selection.
166163
if (isOwnerPermissionError(error, errorMsg)) {
167-
ctx.log(
168-
`Skipping ${org.login}: not an org owner, cannot use 2FA filter. This is expected for orgs the user belongs to but does not administer.`,
164+
ctx.warn(
165+
`Cannot check 2FA for ${orgLogin}: the account must be an organization owner to use the 2FA filter.`,
169166
);
167+
ctx.fail({
168+
title: `Cannot verify 2FA for ${orgLogin}`,
169+
description:
170+
'Insufficient permissions to check 2FA status. The `filter=2fa_disabled` parameter is only available to organization owners on GitHub.',
171+
resourceType: 'organization',
172+
resourceId: orgLogin,
173+
severity: 'medium',
174+
remediation:
175+
'Reconnect the GitHub integration with an account that is an owner of this organization, or remove the org\'s repositories from the selection.',
176+
});
170177
continue;
171178
}
172179

173-
ctx.error(`Failed to check 2FA for ${org.login}: ${errorMsg}`);
180+
ctx.error(`Failed to check 2FA for ${orgLogin}: ${errorMsg}`);
174181
ctx.fail({
175-
title: `Error checking 2FA for ${org.login}`,
182+
title: `Error checking 2FA for ${orgLogin}`,
176183
description: `Failed to query members without 2FA: ${errorMsg}`,
177184
resourceType: 'organization',
178-
resourceId: org.login,
185+
resourceId: orgLogin,
179186
severity: 'medium',
180187
remediation: 'Check the integration connection and try again.',
181188
});
@@ -186,12 +193,12 @@ export const twoFactorAuthCheck: IntegrationCheck = {
186193

187194
if (without2FACount === 0) {
188195
ctx.pass({
189-
title: `All members have 2FA enabled in ${org.login}`,
190-
description: `No members without 2FA were returned for ${org.login}.`,
196+
title: `All members have 2FA enabled in ${orgLogin}`,
197+
description: `No members without 2FA were returned for ${orgLogin}.`,
191198
resourceType: 'organization',
192-
resourceId: org.login,
199+
resourceId: orgLogin,
193200
evidence: {
194-
organization: org.login,
201+
organization: orgLogin,
195202
membersWithout2FA: 0,
196203
checkedAt,
197204
},
@@ -201,13 +208,13 @@ export const twoFactorAuthCheck: IntegrationCheck = {
201208
for (const member of membersWithout2FA) {
202209
ctx.fail({
203210
title: `2FA not enabled: ${member.login}`,
204-
description: `GitHub user @${member.login} in the ${org.login} organization does not have two-factor authentication enabled.`,
211+
description: `GitHub user @${member.login} in the ${orgLogin} organization does not have two-factor authentication enabled.`,
205212
resourceType: 'user',
206-
resourceId: `${org.login}/${member.login}`,
213+
resourceId: `${orgLogin}/${member.login}`,
207214
severity: 'high',
208-
remediation: `Ask @${member.login} to enable 2FA in their GitHub account settings (Settings > Password and authentication > Two-factor authentication). Alternatively, enforce 2FA at the organization level in ${org.login}'s settings.`,
215+
remediation: `Ask @${member.login} to enable 2FA in their GitHub account settings (Settings > Password and authentication > Two-factor authentication). Alternatively, enforce 2FA at the organization level in ${orgLogin}'s settings.`,
209216
evidence: {
210-
organization: org.login,
217+
organization: orgLogin,
211218
username: member.login,
212219
userId: member.id,
213220
profileUrl: member.html_url,
@@ -218,14 +225,14 @@ export const twoFactorAuthCheck: IntegrationCheck = {
218225

219226
// Also emit a summary
220227
ctx.fail({
221-
title: `${without2FACount} member(s) without 2FA in ${org.login}`,
222-
description: `${without2FACount} member(s) in the ${org.login} organization do not have two-factor authentication enabled: ${formatUsernames(membersWithout2FA)}`,
228+
title: `${without2FACount} member(s) without 2FA in ${orgLogin}`,
229+
description: `${without2FACount} member(s) in the ${orgLogin} organization do not have two-factor authentication enabled: ${formatUsernames(membersWithout2FA)}`,
223230
resourceType: 'organization',
224-
resourceId: `${org.login}/2fa-summary`,
231+
resourceId: `${orgLogin}/2fa-summary`,
225232
severity: 'high',
226-
remediation: `1. Go to https://github.com/organizations/${org.login}/settings/security\n2. Under "Authentication security", check "Require two-factor authentication for everyone"\n3. This will require all existing and future members to enable 2FA`,
233+
remediation: `1. Go to https://github.com/organizations/${orgLogin}/settings/security\n2. Under "Authentication security", check "Require two-factor authentication for everyone"\n3. This will require all existing and future members to enable 2FA`,
227234
evidence: {
228-
organization: org.login,
235+
organization: orgLogin,
229236
membersWithout2FA: without2FACount,
230237
usernames: membersWithout2FA.map((member) => member.login),
231238
checkedAt,

0 commit comments

Comments
 (0)