Reports & Incremental Obfuscation

Enterprise feature. Generate detailed obfuscation reports for auditing, stack trace decoding, and incremental builds.

Generating a Report

demeanor --report --report-file MyAppReport.json MyApp.dll

Or in MSBuild:

<ObfuscateReport>true</ObfuscateReport>
<ObfuscateReportFile>$(TargetDir)$(TargetName)Report.json</ObfuscateReportFile>

The report is a JSON file containing every type and member in the obfuscated assembly. For each symbol, it records:

  • Renamed symbols: original name → obfuscated name
  • Excluded symbols: original name + reason why it was not renamed

Report Schema

Each type in the report contains lists of methods, fields, properties, and events. Each symbol has mutually exclusive fields:

// Renamed symbol:
{ "name": "GetCount", "renamed": "a", "accessibility": "public" }

// Excluded symbol:
{ "name": ".ctor", "excludedReason": "runtime special name", "accessibility": "public" }

// Type:
{
  "name": "MyApp.PricingEngine",
  "renamed": "b",
  "visibility": "public",
  "methods": [ ... ],
  "fields": [ ... ]
}

Exclusion Reasons

The excludedReason field explains why a symbol was not renamed:

ReasonMeaning
category disabledA --no-types, --no-methods, etc. flag disabled this category
excluded by --exclude or --xrMatched a command-line exclusion pattern
excluded by [Obfuscation] attributeThe source code has [Obfuscation(Exclude = true)]
name used in string-based reflectionDetected GetMethod("Name") or similar pattern in IL
name used in dynamic dispatchDetected DLR binder usage (dynamic keyword)
public/protected visibilitySymbol is externally visible (use --include-publics to override)
runtime special name.ctor, .cctor, or RTSpecialName flag
native/runtime implementationInternalCall, Native, or Runtime method
virtual override of external methodOverrides a framework method (e.g., Object.ToString)
vararg calling conventionCLI uses name-based lookup for vararg call sites
COM interop type[ComVisible] or [ComImport] — COM uses name-based dispatch
serializable type--no-serializable flag active
enumeration type--no-enumerations flag active
compiler-generated typeAsync state machine, lambda closure, etc.

Incremental Obfuscation

Incremental obfuscation preserves name mappings across versions. When you update your application, existing symbols keep their same obfuscated names while new symbols get new names.

Why use incremental obfuscation?

  • Serialization compatibility: Data serialized with v1's obfuscated type and field names must deserialize correctly in v2. Without incremental mode, a new type inserted before existing types shifts all obfuscated names.
  • Plugin stability: External code may reference obfuscated names stored in configuration files, databases, or DI containers.
  • Smaller patches: Unchanged symbols produce identical metadata bytes, keeping binary diffs small for update distribution.

Workflow

# v1: Initial obfuscation with report
demeanor --report --report-file v1-report.json MyApp.dll

# v2: Incremental obfuscation using v1's report
demeanor --prior-report v1-report.json --report --report-file v2-report.json MyApp.dll

# v3: Chain continues — always use the most recent report
demeanor --prior-report v2-report.json --report --report-file v3-report.json MyApp.dll

How it works

  1. Demeanor loads the prior report and builds a name reservation map: original name → prior obfuscated name.
  2. For each symbol in the current assembly, Demeanor checks if it exists in the prior map.
  3. If found: the prior obfuscated name is reserved in the naming scope and reused.
  4. If not found (new symbol): a new obfuscated name is generated, skipping reserved names.
  5. If a prior name conflicts (e.g., a non-renamable symbol now occupies the prior name): Demeanor assigns a new name and emits a warning.

Adding new types and members

New symbols get fresh obfuscated names. Even if a new type is declared before existing types in source code (shifting TypeDef indices in metadata), the prior report ensures existing symbols keep their original obfuscated names. This is the core problem incremental mode solves — without it, index shifts cause all names to change.

Removing types and members

Removed symbols are simply absent from the new report. Their prior obfuscated names are reserved in the naming scope but never assigned, preventing accidental reuse that could cause cross-version name collisions.

MSBuild integration

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <Obfuscate>true</Obfuscate>
  <ObfuscateReport>true</ObfuscateReport>
  <ObfuscateReportFile>$(TargetDir)$(TargetName)Report.json</ObfuscateReportFile>
  <ObfuscatePriorReport>$(MSBuildProjectDirectory)\prior-report.json</ObfuscatePriorReport>
</PropertyGroup>

Store the report in source control alongside your release. Before each release, copy the current report to prior-report.json and rebuild.

CI/CD workflow

# GitHub Actions example
- name: Download prior report
  uses: actions/download-artifact@v4
  with:
    name: obfuscation-report
    path: prior-report.json
  continue-on-error: true  # First build has no prior report

- name: Build and obfuscate
  env:
    DEMEANOR_LICENSE: ${{ secrets.DEMEANOR_LICENSE }}
  run: dotnet build -c Release

- name: Upload report for next build
  uses: actions/upload-artifact@v4
  with:
    name: obfuscation-report
    path: bin/Release/net10.0/MyAppReport.json