@@ -21,22 +21,57 @@ interface GitHubOrgMember {
2121const MAX_USERNAMES_IN_DESCRIPTION = 20 ;
2222const MAX_USERNAMES_IN_EVIDENCE = 100 ;
2323
24- const isOwnerPermissionError = ( errorMsg : string ) : boolean => {
24+ const getHttpStatus = ( error : unknown ) : number | null => {
25+ if (
26+ typeof error === 'object' &&
27+ error !== null &&
28+ 'status' in error &&
29+ typeof ( error as { status ?: unknown } ) . status === 'number'
30+ ) {
31+ return ( error as { status : number } ) . status ;
32+ }
33+ return null ;
34+ } ;
35+
36+ const isOwnerPermissionError = ( error : unknown , errorMsg : string ) : boolean => {
37+ const status = getHttpStatus ( error ) ;
2538 const lower = errorMsg . toLowerCase ( ) ;
2639
27- if ( lower . includes ( '403' ) || lower . includes ( 'forbidden' ) ) return true ;
40+ // GitHub documents 422 when 2fa_* filters are used in unsupported contexts.
41+ if ( status === 422 ) return true ;
42+
2843 if ( lower . includes ( 'must be an organization owner' ) || lower . includes ( 'organization owners' ) ) {
2944 return true ;
3045 }
3146
32- // GitHub documents 422 for this endpoint when filter constraints fail.
33- if ( lower . includes ( '422' ) || lower . includes ( 'unprocessable' ) || lower . includes ( 'validation failed' ) ) {
47+ if (
48+ lower . includes ( '422' ) ||
49+ lower . includes ( 'unprocessable' ) ||
50+ lower . includes ( 'validation failed' )
51+ ) {
3452 return true ;
3553 }
3654
3755 return false ;
3856} ;
3957
58+ const isSamlSsoError = ( errorMsg : string ) : boolean => {
59+ const lower = errorMsg . toLowerCase ( ) ;
60+ return lower . includes ( 'saml' ) || lower . includes ( 'single sign-on' ) || lower . includes ( 'sso' ) ;
61+ } ;
62+
63+ const isRateLimitError = ( error : unknown , errorMsg : string ) : boolean => {
64+ const status = getHttpStatus ( error ) ;
65+ const lower = errorMsg . toLowerCase ( ) ;
66+
67+ return (
68+ status === 429 ||
69+ lower . includes ( 'rate limit' ) ||
70+ lower . includes ( 'abuse detection' ) ||
71+ ( status === 403 && lower . includes ( 'secondary rate limit' ) )
72+ ) ;
73+ } ;
74+
4075const formatUsernamesPreview = ( members : GitHubOrgMember [ ] ) : string => {
4176 const preview = members . slice ( 0 , MAX_USERNAMES_IN_DESCRIPTION ) . map ( ( member ) => `@${ member . login } ` ) ;
4277 const remaining = members . length - preview . length ;
@@ -106,8 +141,37 @@ export const twoFactorAuthCheck: IntegrationCheck = {
106141 } catch ( error ) {
107142 const errorMsg = error instanceof Error ? error . message : String ( error ) ;
108143
144+ if ( isSamlSsoError ( errorMsg ) ) {
145+ ctx . warn ( `Cannot check 2FA for ${ org . login } : SSO authorization is required.` ) ;
146+ ctx . fail ( {
147+ title : `Cannot verify 2FA for ${ org . login } ` ,
148+ description :
149+ 'GitHub organization SSO authorization is required to access organization members.' ,
150+ resourceType : 'organization' ,
151+ resourceId : org . login ,
152+ severity : 'medium' ,
153+ remediation :
154+ 'Authorize this OAuth app for your organization SSO, then rerun the check.' ,
155+ } ) ;
156+ continue ;
157+ }
158+
159+ if ( isRateLimitError ( error , errorMsg ) ) {
160+ ctx . warn ( `Rate limit reached while checking 2FA for ${ org . login } .` ) ;
161+ ctx . fail ( {
162+ title : `Rate limited while checking ${ org . login } ` ,
163+ description :
164+ 'GitHub rate limits prevented completion of this 2FA check for the organization.' ,
165+ resourceType : 'organization' ,
166+ resourceId : org . login ,
167+ severity : 'low' ,
168+ remediation : 'Wait for the GitHub rate limit to reset, then rerun the check.' ,
169+ } ) ;
170+ continue ;
171+ }
172+
109173 // GitHub returns 422 when the caller is not an org owner for 2fa_* filters.
110- if ( isOwnerPermissionError ( errorMsg ) ) {
174+ if ( isOwnerPermissionError ( error , errorMsg ) ) {
111175 ctx . warn (
112176 `Cannot check 2FA for ${ org . login } : the account must be an organization owner to use the 2FA filter.` ,
113177 ) ;
0 commit comments