diff --git a/Tasks/Common/coveragepublisher/coveragepublisher.ts b/Tasks/Common/coveragepublisher/coveragepublisher.ts index 292cf7571b65..1a621653f766 100644 --- a/Tasks/Common/coveragepublisher/coveragepublisher.ts +++ b/Tasks/Common/coveragepublisher/coveragepublisher.ts @@ -64,6 +64,9 @@ async function publishCoverage(inputFiles: string[], reportDirectory: string, pa } try { + // Get comprehensive proxy configuration to fix .NET HttpClient proxy issues + const proxyConfig = getProxyEnvironmentVariables(); + const env = { "SYSTEM_ACCESSTOKEN": taskLib.getEndpointAuthorizationParameter('SystemVssConnection', 'AccessToken', false), "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI": taskLib.getVariable('System.TeamFoundationCollectionUri'), @@ -72,9 +75,9 @@ async function publishCoverage(inputFiles: string[], reportDirectory: string, pa "AGENT_TEMPPATH": taskLib.getVariable('Agent.TempPath'), "SYSTEM_TEAMPROJECTID": taskLib.getVariable('System.TeamProjectId'), "PIPELINES_COVERAGEPUBLISHER_DEBUG": taskLib.getVariable('PIPELINES_COVERAGEPUBLISHER_DEBUG'), - "HTTPS_PROXY": process.env['HTTPS_PROXY'], - "NO_PROXY": process.env['NO_PROXY'], - "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT": taskLib.getVariable('DOTNET_SYSTEM_GLOBALIZATION_INVARIANT') + "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT": taskLib.getVariable('DOTNET_SYSTEM_GLOBALIZATION_INVARIANT'), + // Comprehensive proxy configuration for .NET HttpClient + ...proxyConfig }; await dotnet.exec({ @@ -96,6 +99,132 @@ async function publishCoverage(inputFiles: string[], reportDirectory: string, pa } +function getProxyEnvironmentVariables(): { [key: string]: string } { + const proxyVars: { [key: string]: string } = {}; + + // Get Azure Pipelines agent proxy configuration (highest priority) + const agentProxyUrl = taskLib.getVariable("agent.proxyurl"); + const agentProxyUsername = taskLib.getVariable("agent.proxyusername"); + const agentProxyPassword = taskLib.getVariable("agent.proxypassword"); + const agentProxyBypass = taskLib.getVariable("agent.proxybypasslist"); + + // Input validation and sanitization + if (agentProxyUrl && !isValidProxyUrl(agentProxyUrl)) { + taskLib.warning("Invalid proxy URL format detected, skipping agent proxy configuration"); + return proxyVars; + } + + // Construct proxy URL with authentication if available + let proxyUrl = ""; + if (agentProxyUrl) { + if (agentProxyUsername && agentProxyPassword) { + // Validate credentials contain no malicious characters + if (!isValidCredential(agentProxyUsername) || !isValidCredential(agentProxyPassword)) { + taskLib.warning("Invalid characters detected in proxy credentials, using proxy without authentication"); + proxyUrl = agentProxyUrl; + } else { + // Add authentication to proxy URL + try { + const url = new URL(agentProxyUrl); + url.username = encodeURIComponent(agentProxyUsername); + url.password = encodeURIComponent(agentProxyPassword); + proxyUrl = url.toString(); + taskLib.debug(`Using agent proxy with authentication: ${getMaskedProxyUrl(proxyUrl)}`); + } catch (err) { + proxyUrl = agentProxyUrl; + taskLib.warning(`Failed to parse agent proxy URL, using without authentication: ${err}`); + } + } + } else { + proxyUrl = agentProxyUrl; + taskLib.debug(`Using agent proxy: ${getMaskedProxyUrl(agentProxyUrl)}`); + } + } else { + // Fall back to environment variables + proxyUrl = process.env['HTTPS_PROXY'] || process.env['https_proxy'] || + process.env['HTTP_PROXY'] || process.env['http_proxy'] || ""; + if (proxyUrl) { + taskLib.debug(`Using environment proxy: ${getMaskedProxyUrl(proxyUrl)}`); + } + } + // Set comprehensive proxy environment variables for .NET + if (proxyUrl) { + // Standard proxy environment variables (case variations for compatibility) + proxyVars["HTTP_PROXY"] = proxyUrl; + proxyVars["HTTPS_PROXY"] = proxyUrl; + proxyVars["http_proxy"] = proxyUrl; + proxyVars["https_proxy"] = proxyUrl; + + // .NET specific environment variables to force proxy initialization + // These are critical for resolving the HttpClient.DefaultProxy initialization issue + proxyVars["DOTNET_SYSTEM_NET_HTTP_USEDEFAULTCREDENTIALS"] = "true"; + proxyVars["DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER"] = "false"; // Forces WinHttpHandler on Windows + } + + // Handle NO_PROXY / bypass list + let noProxyList = ""; + if (agentProxyBypass) { + noProxyList = agentProxyBypass; + taskLib.debug(`Using agent proxy bypass list: ${agentProxyBypass}`); + } else { + noProxyList = process.env['NO_PROXY'] || process.env['no_proxy'] || ""; + if (noProxyList) { + taskLib.debug(`Using environment proxy bypass list: ${noProxyList}`); + } + } + + if (noProxyList) { + proxyVars["NO_PROXY"] = noProxyList; + proxyVars["no_proxy"] = noProxyList; + } + + // Log configuration for debugging (with enhanced credential masking) + if (proxyUrl) { + taskLib.debug(`Proxy configuration set for .NET application: ${getMaskedProxyUrl(proxyUrl)}`); + if (noProxyList) { + taskLib.debug(`Proxy bypass list: ${noProxyList}`); + } + } else { + taskLib.debug("No proxy configuration detected"); + } + + // Security note: Environment variables will contain proxy credentials + // Ensure child process cleans up these variables after use + return proxyVars; +} + +// Security helper functions +function isValidProxyUrl(url: string): boolean { + try { + const parsedUrl = new URL(url); + return ['http:', 'https:'].includes(parsedUrl.protocol); + } catch { + return false; + } +} + +function isValidCredential(credential: string): boolean { + // Basic validation to prevent injection attacks + // Reject credentials containing potentially dangerous characters + const dangerousChars = /[\r\n\0\x1f<>"'`\\]/; + return !dangerousChars.test(credential) && credential.length <= 256; +} + +function getMaskedProxyUrl(url: string): string { + if (!url) return url; + try { + const parsedUrl = new URL(url); + if (parsedUrl.username || parsedUrl.password) { + // Mask both username and password for security + return `${parsedUrl.protocol}//***:***@${parsedUrl.host}${parsedUrl.pathname}${parsedUrl.search}`; + } + return url; + } catch { + // If URL parsing fails, mask any potential credentials pattern + return url.replace(/\/\/[^@]*@/, '//***:***@'); + } +} + function isNullOrWhitespace(input: any) { if (typeof input === 'undefined' || input == null) { return true; diff --git a/Tasks/PublishCodeCoverageResultsV2/Tests/L0.ts b/Tasks/PublishCodeCoverageResultsV2/Tests/L0.ts index be4bdc857175..b76128412ee6 100644 --- a/Tasks/PublishCodeCoverageResultsV2/Tests/L0.ts +++ b/Tasks/PublishCodeCoverageResultsV2/Tests/L0.ts @@ -26,4 +26,14 @@ describe('PublishCodeCoverageResultsV2 Suite', function () { assert(tr.succeeded, 'task should have succeeded'); // It will give a message of No code coverage for empty inputs }); + // New proxy configuration tests + it('Should handle agent proxy configuration correctly', async function() { + const testPath = path.join(__dirname, 'L0ProxyAgentConfig.ts'); + const tr: MockTestRunner = new MockTestRunner(testPath); + await tr.runAsync(); + + // Verify proxy environment variables are set correctly + assert(tr.succeeded || tr.stdout.indexOf('Using agent proxy') >= 0, 'Should configure agent proxy'); + }); + }); diff --git a/Tasks/PublishCodeCoverageResultsV2/task.json b/Tasks/PublishCodeCoverageResultsV2/task.json index 9f1f0da5d970..e3ed6283c98c 100644 --- a/Tasks/PublishCodeCoverageResultsV2/task.json +++ b/Tasks/PublishCodeCoverageResultsV2/task.json @@ -16,7 +16,7 @@ "author": "Microsoft Corporation", "version": { "Major": 2, - "Minor": 264, + "Minor": 265, "Patch": 0 }, "demands": [], diff --git a/Tasks/PublishCodeCoverageResultsV2/task.loc.json b/Tasks/PublishCodeCoverageResultsV2/task.loc.json index 17f40945c27d..fde526ae9b5d 100644 --- a/Tasks/PublishCodeCoverageResultsV2/task.loc.json +++ b/Tasks/PublishCodeCoverageResultsV2/task.loc.json @@ -16,7 +16,7 @@ "author": "Microsoft Corporation", "version": { "Major": 2, - "Minor": 264, + "Minor": 265, "Patch": 0 }, "demands": [], diff --git a/package-lock.json b/package-lock.json index a05ae791b7b7..207a26ae43ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,6 @@ "integrity": "sha1-EqVQuHlEUt9MiwhPlQA7zhdC1JY=", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -612,7 +611,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211",