Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/ISSUE_TEMPLATE/bug-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,11 @@ body:

Windows (PowerShell):
$env:ARTIFACTS_CREDENTIALPROVIDER_LOG_PATH = "$PWD\\artifacts-credprovider.log"
$env:ARTIFACTS_CREDENTIALPROVIDER_REDACT_ENABLED = "true" # Optional: redact sensitive info

Linux/macOS (bash/zsh):
export ARTIFACTS_CREDENTIALPROVIDER_LOG_PATH="$PWD/artifacts-credprovider.log"
export ARTIFACTS_CREDENTIALPROVIDER_REDACT_ENABLED="true" # Optional: redact sensitive info

2) Run your failing command with detailed verbosity:

Expand All @@ -83,7 +85,8 @@ body:
3) Provide the log output below with any urls or secrets redacted.
Note:
- Legacy log path variable is NUGET_CREDENTIALPROVIDER_LOG_PATH.
- Remove/redact urls before posting logs.
- Setting ARTIFACTS_CREDENTIALPROVIDER_REDACT_ENABLED=true provides best-effort automatic redaction.
- Redaction is not guaranteed to catch everything. ALWAYS review logs before posting to ensure no sensitive data is exposed.
validations:
required: false

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft. All rights reserved.
//
// Licensed under the MIT license.

using System;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NuGet.Common;
using NuGetCredentialProvider.Logging;

namespace CredentialProvider.Microsoft.Tests.Logging
{
[TestClass]
public class LogEveryMessageFileLoggerTests
{
private string tempLogFile;

[TestInitialize]
public void Setup()
{
tempLogFile = Path.GetTempFileName();
}

[TestCleanup]
public void Cleanup()
{
if (File.Exists(tempLogFile))
{
try
{
File.Delete(tempLogFile);
}
catch
{
// Ignore cleanup errors
}
}
}

[TestMethod]
public void Log_WritesMessageToFile()
{
// Arrange
var logger = new LogEveryMessageFileLogger(tempLogFile);
var message = "Test message";

// Act
logger.Log(LogLevel.Information, allowOnConsole: false, message);

// Assert
var logContent = File.ReadAllText(tempLogFile);
Assert.IsTrue(logContent.Contains("Test message"), "Message should be written to file");
}

[TestMethod]
public void Log_PreservesNonSensitiveContent()
{
// Arrange
var logger = new LogEveryMessageFileLogger(tempLogFile);
var message = "Successfully authenticated to service";

// Act
logger.Log(LogLevel.Information, allowOnConsole: false, message);

// Assert
var logContent = File.ReadAllText(tempLogFile);
Assert.IsTrue(logContent.Contains("Successfully authenticated to service"), "Non-sensitive content should be preserved");
}

[TestMethod]
public void Log_HandlesNullMessage()
{
// Arrange
var logger = new LogEveryMessageFileLogger(tempLogFile);

// Act & Assert - should not throw
logger.Log(LogLevel.Verbose, allowOnConsole: false, null);
}
}
}
220 changes: 220 additions & 0 deletions CredentialProvider.Microsoft.Tests/Util/RedactionUtilTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// Copyright (c) Microsoft. All rights reserved.
//
// Licensed under the MIT license.

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NuGetCredentialProvider.Util;

namespace CredentialProvider.Microsoft.Tests.Util
{
[TestClass]
public class RedactionUtilTests
{
[TestInitialize]
public void Setup()
{
// Reset redaction flag before each test
RedactionUtil.ShouldRedact = false;
}

[TestMethod]
public void RedactFeedUrl_WhenRedactionDisabled_ReturnsOriginalUrl()
{
// Arrange
RedactionUtil.ShouldRedact = false;
var url = "https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json";

// Act
var result = RedactionUtil.RedactFeedUrl(url);

// Assert
Assert.AreEqual(url, result);
}

[TestMethod]
public void RedactFeedUrl_WhenRedactionEnabled_RedactsAzureDevOpsUrl()
{
// Arrange
RedactionUtil.ShouldRedact = true;
var url = "https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json";

// Act
var result = RedactionUtil.RedactFeedUrl(url);

// Assert
Assert.IsTrue(result.Contains("[REDACTED_FEED_URL]"));
Assert.IsFalse(result.Contains("myorg"));
Assert.IsFalse(result.Contains("myfeed"));
}

[TestMethod]
public void RedactFeedUrl_WhenRedactionEnabled_RedactsVisualStudioUrl()
{
// Arrange
RedactionUtil.ShouldRedact = true;
var url = "https://myorg.pkgs.visualstudio.com/_packaging/myfeed/nuget/v3/index.json";

// Act
var result = RedactionUtil.RedactFeedUrl(url);

// Assert
Assert.IsTrue(result.Contains("[REDACTED_FEED_URL]"));
Assert.IsFalse(result.Contains("myorg"));
Assert.IsFalse(result.Contains("myfeed"));
}

[TestMethod]
public void RedactFeedUrl_WhenRedactionEnabled_RedactsDevAzureComUrl()
{
// Arrange
RedactionUtil.ShouldRedact = true;
var url = "https://dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json";

// Act
var result = RedactionUtil.RedactFeedUrl(url);

// Assert
Assert.IsTrue(result.Contains("[REDACTED_FEED_URL]"));
Assert.IsFalse(result.Contains("myorg"));
Assert.IsFalse(result.Contains("myfeed"));
}

[TestMethod]
public void RedactFeedUrl_WhenRedactionEnabled_RedactsOnPremUrl()
{
// Arrange
RedactionUtil.ShouldRedact = true;
var url = "https://myserver.company.com/tfs/DefaultCollection/_packaging/myfeed/nuget/v3/index.json";

// Act
var result = RedactionUtil.RedactFeedUrl(url);

// Assert
Assert.IsTrue(result.Contains("[REDACTED_FEED_URL]"));
Assert.IsFalse(result.Contains("myserver"));
Assert.IsFalse(result.Contains("DefaultCollection"));
Assert.IsFalse(result.Contains("myfeed"));
}

[TestMethod]
public void RedactFeedUrl_WithUri_WhenRedactionEnabled_RedactsUrl()
{
// Arrange
RedactionUtil.ShouldRedact = true;
var uri = new Uri("https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json");

// Act
var result = RedactionUtil.RedactFeedUrl(uri);

// Assert
Assert.IsTrue(result.Contains("[REDACTED_FEED_URL]"));
Assert.IsFalse(result.Contains("myorg"));
}

[TestMethod]
public void RedactFeedUrl_WithNullUri_ReturnsNull()
{
// Arrange
RedactionUtil.ShouldRedact = true;
Uri uri = null;

// Act
var result = RedactionUtil.RedactFeedUrl(uri);

// Assert
Assert.IsNull(result);
}

[TestMethod]
public void ShouldRedact_CanBeSetAndRetrieved()
{
// Arrange & Act
RedactionUtil.ShouldRedact = true;

// Assert
Assert.IsTrue(RedactionUtil.ShouldRedact);

// Arrange & Act
RedactionUtil.ShouldRedact = false;

// Assert
Assert.IsFalse(RedactionUtil.ShouldRedact);
}

[TestMethod]
public void RedactFeedUrl_WhenRedactionEnabled_RedactsAllUrls()
{
// Arrange
RedactionUtil.ShouldRedact = true;

// Act & Assert - Various feed URLs should all be redacted
var nugetOrg = RedactionUtil.RedactFeedUrl("https://api.nuget.org/v3/index.json");
Assert.AreEqual("https://[REDACTED_FEED_URL]", nugetOrg);

var github = RedactionUtil.RedactFeedUrl("https://nuget.pkg.github.com/myorg/index.json");
Assert.AreEqual("https://[REDACTED_FEED_URL]", github);

var myget = RedactionUtil.RedactFeedUrl("https://www.myget.org/F/myfeed/api/v3/index.json");
Assert.AreEqual("https://[REDACTED_FEED_URL]", myget);

var privateServer = RedactionUtil.RedactFeedUrl("https://packages.internal.company.com/nuget/v3/index.json");
Assert.AreEqual("https://[REDACTED_FEED_URL]", privateServer);
}

[TestMethod]
public void RedactPassword_WhenRedactionDisabled_ReturnsOriginalPassword()
{
// Arrange
RedactionUtil.ShouldRedact = false;
var password = "abc123secretToken";

// Act
var result = RedactionUtil.RedactPassword(password);

// Assert
Assert.AreEqual(password, result);
}

[TestMethod]
public void RedactPassword_WhenRedactionEnabled_RedactsPassword()
{
// Arrange
RedactionUtil.ShouldRedact = true;
var password = "abc123secretToken";

// Act
var result = RedactionUtil.RedactPassword(password);

// Assert
Assert.AreEqual("[REDACTED]", result);
Assert.AreNotEqual(password, result);
}

[TestMethod]
public void RedactPassword_WithNull_ReturnsNull()
{
// Arrange
RedactionUtil.ShouldRedact = true;

// Act
var result = RedactionUtil.RedactPassword(null);

// Assert
Assert.IsNull(result);
}

[TestMethod]
public void RedactPassword_WithEmptyString_ReturnsEmptyString()
{
// Arrange
RedactionUtil.ShouldRedact = true;

// Act
var result = RedactionUtil.RedactPassword("");

// Assert
Assert.AreEqual("", result);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
<DefineConstants>$(DefineConstants);TRACE</DefineConstants>
</PropertyGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>CredentialProvider.Microsoft.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.VisualStudioEng.MicroBuild.Core" PrivateAssets="All" />
Expand Down
2 changes: 1 addition & 1 deletion CredentialProvider.Microsoft/CredentialProviderArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ internal class CredentialProviderArgs
[ArgDescription("Display this amount of detail in the output")]
public LogLevel Verbosity { get; set; }

[ArgDescription("Prevents writing the password to standard output (for troubleshooting purposes)")]
[ArgDescription("Redacts sensitive information from output: passwords/tokens and feed URLs. This is a best-effort feature. Always review logs before sharing.")]
public bool RedactPassword { get; set; }

[ArgShortcut("?")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public async Task<Uri> GetAuthorizationEndpoint(Uri uri, CancellationToken cance
logger.Warning(e.StackTrace);
}

logger.Warning(string.Format(Resources.SPSAuthEndpointNotFound, uri.ToString()));
logger.Warning(string.Format(Resources.SPSAuthEndpointNotFound, RedactionUtil.RedactFeedUrl(uri.ToString())));
return null;
}

Expand All @@ -117,7 +117,7 @@ protected virtual async Task<HttpResponseHeaders> GetResponseHeadersAsync(Uri ur

using (var request = new HttpRequestMessage(HttpMethod.Get, uri))
{
logger.Verbose($"GET {uri}");
logger.Verbose($"GET {RedactionUtil.RedactFeedUrl(uri)}");
using (var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
{
cache[uri] = response.Headers;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public override async Task<GetAuthenticationCredentialsResponse> HandleRequestAs
InteractiveTimeout = TimeSpan.FromSeconds(EnvUtil.GetDeviceFlowTimeoutFromEnvironmentInSeconds(Logger)),
DeviceCodeResultCallback = (DeviceCodeResult deviceCodeResult) =>
{
Logger.Minimal(string.Format(Resources.DeviceFlowRequestedResource, request.Uri.ToString()));
Logger.Minimal(string.Format(Resources.DeviceFlowRequestedResource, RedactionUtil.RedactFeedUrl(request.Uri.ToString())));
Logger.Minimal(string.Format(Resources.DeviceFlowMessage, deviceCodeResult.VerificationUrl, deviceCodeResult.UserCode));

return Task.CompletedTask;
Expand Down Expand Up @@ -172,7 +172,7 @@ public override async Task<GetAuthenticationCredentialsResponse> HandleRequestAs

if (!string.IsNullOrWhiteSpace(sessionToken))
{
Verbose(string.Format(Resources.VSTSSessionTokenCreated, request.Uri.AbsoluteUri));
Verbose(string.Format(Resources.VSTSSessionTokenCreated, RedactionUtil.RedactFeedUrl(request.Uri)));
return new GetAuthenticationCredentialsResponse(
Username,
sessionToken,
Expand All @@ -183,11 +183,11 @@ public override async Task<GetAuthenticationCredentialsResponse> HandleRequestAs
}
catch (Exception e)
{
Verbose(string.Format(Resources.VSTSCreateSessionException, request.Uri.AbsoluteUri, e.Message, e.StackTrace));
Verbose(string.Format(Resources.VSTSCreateSessionException, RedactionUtil.RedactFeedUrl(request.Uri), e.Message, e.StackTrace));
}
}

Verbose(string.Format(Resources.VSTSCredentialsNotFound, request.Uri.AbsoluteUri));
Verbose(string.Format(Resources.VSTSCredentialsNotFound, RedactionUtil.RedactFeedUrl(request.Uri)));
return null;
}
}
Expand Down
Loading