From eb53746ad5a1d7c682c180434c8fae97b7df4bff Mon Sep 17 00:00:00 2001 From: spec k Date: Sun, 15 Mar 2026 21:42:31 -0600 Subject: [PATCH] fix: fall back to workflow scanning when code scanning API returns 403 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The getCodeScanningStatus function previously returned early with ghas-required or permission-denied when the code-scanning API returned 403, skipping workflow file scanning entirely. This prevented detection of third-party SAST tools (Semgrep, Snyk, Trivy) that upload SARIF results via github/codeql-action/upload-sarif. The code-scanning API requires GHAS for private repos, but reading workflow file contents only requires contents:read — these are independent permission scopes. A 403 on the API should not prevent checking for code scanning workflows. Now the 403 handler sets a flag instead of returning early, always falls through to workflow scanning, and only returns the appropriate error status if no workflows are found either. --- .../github/checks/sanitized-inputs.ts | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/integration-platform/src/manifests/github/checks/sanitized-inputs.ts b/packages/integration-platform/src/manifests/github/checks/sanitized-inputs.ts index ac5c7f2f41..09a76496ef 100644 --- a/packages/integration-platform/src/manifests/github/checks/sanitized-inputs.ts +++ b/packages/integration-platform/src/manifests/github/checks/sanitized-inputs.ts @@ -237,6 +237,8 @@ export const sanitizedInputsCheck: IntegrationCheck = { isPrivate: boolean; isGhasEnabled: boolean; }): Promise => { + let apiGot403 = false; + // First, try the default setup API try { const setup = await ctx.fetch( @@ -252,32 +254,23 @@ export const sanitizedInputsCheck: IntegrationCheck = { } catch (error) { const errorStr = String(error); - // Check for 403 Forbidden - indicates permission issues or GHAS not enabled - // Return early without checking workflows since we can't verify they're actually running if (errorStr.includes('403') || errorStr.includes('Forbidden')) { + // The code-scanning API requires GHAS for private repos, but reading + // workflow file contents only requires contents:read. A 403 here does + // not mean we can't check for code scanning workflows. ctx.log( - `Code scanning API returned 403 for ${repoName} (private: ${isPrivate}, ghas: ${isGhasEnabled})`, + `Code scanning API returned 403 for ${repoName} (private: ${isPrivate}, ghas: ${isGhasEnabled}). Falling back to workflow file scanning.`, ); - - // For private repos, distinguish between GHAS not enabled vs permission issue - if (isPrivate) { - if (isGhasEnabled) { - // GHAS is enabled but we still got 403 - it's a permission issue - return { status: 'permission-denied', isPrivate }; - } - // GHAS is not enabled - that's why we got 403 - return { status: 'ghas-required' }; - } - - return { status: 'permission-denied', isPrivate }; + apiGot403 = true; + } else { + // Other errors - API might not be available, continue to check workflows + ctx.log(`Code scanning default setup not available for ${repoName}: ${errorStr}`); } - - // Other errors - API might not be available, continue to check workflows - ctx.log(`Code scanning default setup not available for ${repoName}: ${errorStr}`); } // Fall back to checking for workflow files with code scanning - // Only reached if API didn't return 403 (meaning we have access but default setup isn't on) + // This catches repos using third-party SAST tools (Semgrep, Snyk, Trivy, etc.) + // that upload SARIF results via github/codeql-action/upload-sarif const codeScanningWorkflows = await findCodeScanningWorkflows(repoName, tree); if (codeScanningWorkflows.length > 0) { return { @@ -287,6 +280,17 @@ export const sanitizedInputsCheck: IntegrationCheck = { }; } + // No workflows found either - determine appropriate failure status + if (apiGot403) { + if (isPrivate) { + if (isGhasEnabled) { + return { status: 'permission-denied', isPrivate }; + } + return { status: 'ghas-required' }; + } + return { status: 'permission-denied', isPrivate }; + } + return { status: 'not-configured' }; };