Skip to content
Merged
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
42 changes: 38 additions & 4 deletions Confuser.CLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq;
using System.Xml;
using Confuser.Core;
using Confuser.Core.Diagnostics;
using Confuser.Core.Project;
using Microsoft.Extensions.Logging;
using NDesk.Options;
Expand All @@ -25,6 +26,8 @@ static int Main(string[] args) {
bool noPause = false;
bool debug = false;
bool quiet = false;
bool dumpRequested = false;
string dumpPath = null;
int verbosity = 0;
string outDir = null;
string snKeyPath = null;
Expand Down Expand Up @@ -59,6 +62,9 @@ static int Main(string[] args) {
}, {
"q|quiet", "only show warnings and errors.",
value => { quiet = (value != null); }
}, {
"dump:", "write a diagnostic report (optionally to the given file).",
value => { dumpRequested = true; if (!string.IsNullOrEmpty(value)) dumpPath = value; }
}
};

Expand Down Expand Up @@ -141,7 +147,7 @@ static int Main(string[] args) {
parameters.Project = proj;
}

int retVal = RunProject(parameters, quiet, verbosity);
int retVal = RunProject(parameters, quiet, verbosity, dumpRequested, dumpPath);

if (NeedPause() && !noPause) {
Console.WriteLine("Press any key to continue...");
Expand Down Expand Up @@ -203,7 +209,7 @@ static void LoadTemplateProject(string templatePath, ConfuserProject proj, List<
templateModules.Add(templateModule);
}

static int RunProject(ConfuserParameters parameters, bool quiet, int verbosity) {
static int RunProject(ConfuserParameters parameters, bool quiet, int verbosity, bool dumpRequested, string dumpPath) {
var levelSwitch = quiet
? LogEventLevel.Warning
: verbosity >= 3 ? LogEventLevel.Verbose
Expand All @@ -222,17 +228,44 @@ static int RunProject(ConfuserParameters parameters, bool quiet, int verbosity)
var melLogger = loggerFactory.CreateLogger("ConfuserEx");

var progressReporter = new ConsoleProgressReporter();
parameters.Logger = melLogger;
parameters.ProgressReporter = progressReporter;

// When a diagnostic report is requested, wrap both the logger and the progress reporter
// with a collector so the report captures the full transcript, timing and outcome even
// when the run fails.
DiagnosticCollector collector = null;
if (dumpRequested) {
collector = new DiagnosticCollector(melLogger, progressReporter) { Project = parameters.Project };
parameters.Logger = collector;
parameters.ProgressReporter = collector;
}
else {
parameters.Logger = melLogger;
parameters.ProgressReporter = progressReporter;
}

if (OperatingSystem.IsWindows())
Console.Title = "ConfuserEx - Running...";
ConfuserEngine.Run(parameters).GetAwaiter().GetResult();

Log.CloseAndFlush();

if (collector != null)
WriteDiagnosticReport(collector, dumpPath);

return progressReporter.ReturnValue;
}

static void WriteDiagnosticReport(DiagnosticCollector collector, string dumpPath) {
string path = string.IsNullOrEmpty(dumpPath) ? "confuser-diagnostic-report.md" : dumpPath;
try {
File.WriteAllText(path, collector.GenerateReport());
WriteLineWithColor(ConsoleColor.Cyan, "Diagnostic report written to: " + Path.GetFullPath(path));
}
catch (Exception ex) {
WriteLineWithColor(ConsoleColor.Red, "Failed to write diagnostic report: " + ex.Message);
}
}

static bool NeedPause() {
return Debugger.IsAttached || string.IsNullOrEmpty(Environment.GetEnvironmentVariable("PROMPT"));
}
Expand All @@ -250,6 +283,7 @@ static void PrintUsage() {
WriteLine(" -snkeypass : specifies strong name key password.");
WriteLine(" -v|verbose : increase verbosity (-v debug, -vv trace).");
WriteLine(" -q|quiet : only show warnings and errors.");
WriteLine(" -dump : write a diagnostic report (-dump=<file> for a custom path).");
}

static void WriteLineWithColor(ConsoleColor color, string txt) {
Expand Down
163 changes: 163 additions & 0 deletions Confuser.Core/Diagnostics/DiagnosticCollector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using Confuser.Core.Project;
using Microsoft.Extensions.Logging;

namespace Confuser.Core.Diagnostics {
/// <summary>
/// A single captured log entry, rendered to text at capture time.
/// </summary>
public readonly struct DiagnosticLogEntry {
public DiagnosticLogEntry(LogLevel level, string message, string exception) {
Level = level;
Message = message;
Exception = exception;
}

/// <summary>The severity of the entry.</summary>
public LogLevel Level { get; }

/// <summary>The rendered message text.</summary>
public string Message { get; }

/// <summary>The rendered exception (including stack trace), or <c>null</c> if none.</summary>
public string Exception { get; }
}

/// <summary>
/// Wraps the real <see cref="ILogger" /> and <see cref="IProgressReporter" /> used during an
/// obfuscation run, passing every call through while capturing a full-verbosity transcript,
/// timing and outcome. On completion — success or failure — it can produce a self-contained
/// markdown diagnostic report suitable for a bug report.
/// </summary>
/// <remarks>
/// <para>
/// Capture is intentionally independent of the inner logger's level: the collector reports
/// <see cref="IsEnabled" /> as <c>true</c> and keeps every entry, so a report from a default
/// (Information) run still contains the Debug detail needed to diagnose a failure. Display
/// filtering is preserved because entries are only forwarded to the inner logger when it is
/// enabled for that level.
/// </para>
/// <para>
/// The entry buffer is bounded (see <see cref="DefaultCapacity" />); once full, the oldest
/// entries are dropped and counted in <see cref="DroppedCount" /> so the report can note the
/// loss rather than silently mislead.
/// </para>
/// <para>
/// <see cref="Finish" /> is last-wins: a packer runs a nested engine pass with the same
/// collector, so the top-level run — which finishes last — determines the reported outcome
/// and elapsed time.
/// </para>
/// </remarks>
public sealed class DiagnosticCollector : ILogger, IProgressReporter {
/// <summary>The default maximum number of log entries retained.</summary>
public const int DefaultCapacity = 2000;

readonly ILogger inner;
readonly IProgressReporter innerReporter;
readonly int capacity;
readonly object sync = new object();
readonly Queue<DiagnosticLogEntry> entries;
readonly DateTime begin = DateTime.UtcNow;
int dropped;
bool? successful;
TimeSpan elapsed;

/// <summary>
/// Initializes a new collector.
/// </summary>
/// <param name="inner">The logger to forward display output to. Required.</param>
/// <param name="innerReporter">The progress reporter to forward to, or <c>null</c>.</param>
/// <param name="capacity">The maximum number of log entries to retain.</param>
public DiagnosticCollector(ILogger inner, IProgressReporter innerReporter = null, int capacity = DefaultCapacity) {
this.inner = inner ?? throw new ArgumentNullException(nameof(inner));
this.innerReporter = innerReporter;
this.capacity = capacity < 1 ? 1 : capacity;
entries = new Queue<DiagnosticLogEntry>(Math.Min(this.capacity, 64));
}

/// <summary>
/// The project being processed, used to populate the report's configuration section.
/// </summary>
public ConfuserProject Project { get; set; }

/// <summary>
/// The run outcome: <c>true</c> on success, <c>false</c> on failure, <c>null</c> if the run
/// never reported completion.
/// </summary>
public bool? Successful {
get { lock (sync) return successful; }
}

/// <summary>The elapsed time recorded at the last <see cref="Finish" /> call.</summary>
public TimeSpan Elapsed {
get { lock (sync) return elapsed; }
}

/// <summary>The number of log entries dropped because the buffer was full.</summary>
public int DroppedCount {
get { lock (sync) return dropped; }
}

/// <summary>
/// Returns an immutable copy of the currently retained log entries, oldest first.
/// </summary>
public IReadOnlyList<DiagnosticLogEntry> Snapshot() {
lock (sync) return new List<DiagnosticLogEntry>(entries);
}

/// <summary>
/// Produces the markdown diagnostic report. Never throws.
/// </summary>
public string GenerateReport() => DiagnosticReport.Generate(this);

#region ILogger

public IDisposable BeginScope<TState>(TState state) => inner.BeginScope(state);

public bool IsEnabled(LogLevel logLevel) => true;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
Func<TState, Exception, string> formatter) {
string message;
try {
message = formatter != null ? formatter(state, exception) : state?.ToString() ?? string.Empty;
}
catch {
message = state?.ToString() ?? string.Empty;
}

var entry = new DiagnosticLogEntry(logLevel, message, exception?.ToString());
lock (sync) {
entries.Enqueue(entry);
while (entries.Count > capacity) {
entries.Dequeue();
dropped++;
}
}

// Forward to the inner logger for display; it applies its own level filter.
if (inner.IsEnabled(logLevel))
inner.Log(logLevel, eventId, state, exception, formatter);
}

#endregion

#region IProgressReporter

public void Progress(int progress, int overall) => innerReporter?.Progress(progress, overall);

public void EndProgress() => innerReporter?.EndProgress();

public void Finish(bool successful) {
lock (sync) {
this.successful = successful;
elapsed = DateTime.UtcNow - begin;
}

innerReporter?.Finish(successful);
}

#endregion
}
}
52 changes: 52 additions & 0 deletions Confuser.Core/Diagnostics/DiagnosticRedactor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Text;

namespace Confuser.Core.Diagnostics {
/// <summary>
/// Scrubs sensitive information from text destined for a diagnostic report.
/// </summary>
/// <remarks>
/// Diagnostic reports are meant to be pasted into public issue trackers, so any text
/// that flows into one must have the reporter's identity removed. The most common leak
/// is the user-profile path (e.g. <c>C:\Users\alice\...</c>) which appears in absolute
/// paths throughout log output and project configuration.
/// </remarks>
public static class DiagnosticRedactor {
/// <summary>
/// The placeholder substituted for the user-profile directory.
/// </summary>
public const string UserPlaceholder = "%USER%";

/// <summary>
/// Replaces every occurrence of the user-profile directory in <paramref name="text" />
/// with <see cref="UserPlaceholder" />. The match is case-insensitive because Windows
/// paths are.
/// </summary>
/// <param name="text">The text to scrub. Returned unchanged if <c>null</c> or empty.</param>
/// <param name="userProfile">The user-profile directory to redact, or <c>null</c> to skip.</param>
/// <returns>The scrubbed text.</returns>
public static string Redact(string text, string userProfile) {
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(userProfile))
return text;
return ReplaceCaseInsensitive(text, userProfile, UserPlaceholder);
}

static string ReplaceCaseInsensitive(string input, string search, string replacement) {
var sb = new StringBuilder(input.Length);
int index = 0;
while (true) {
int found = input.IndexOf(search, index, StringComparison.OrdinalIgnoreCase);
if (found < 0) {
sb.Append(input, index, input.Length - index);
break;
}

sb.Append(input, index, found - index);
sb.Append(replacement);
index = found + search.Length;
}

return sb.ToString();
}
}
}
Loading
Loading