Skip to content

Releases: fluentfoundation/redecker

v0.4.0 — the corpus starts proposing rules

Choose a tag to compare

@jzabroski jzabroski released this 01 Aug 02:08

Two new rules. One exists because a measurement said restore lets the failure through; the other
exists because the corpus suggested it, which is a first.

Install

dotnet tool install --global dotnet-redecker    # investigate an upgrade
dotnet add package Redecker.MSBuild             # fail the build instead

📖 Documentation

New rules

Rule What it checks
RDK0011 package left behind — a provider still on last year's release while the thing it constrains moved
RDK0012 stable package depends on a prerelease

RDK0011 — and the question that nearly killed it

You bump EF Core. Your database provider stays where it is. The obvious objection to a rule here is
that NuGet already raises NU1608, so the rule would be redundant.

Rather than answer that from memory, four throwaway projects and a real dotnet restore:

What was pinned Diagnostic Restore Build
Provider declaring an unbounded minimum (8.0.11) none at all
Pomelo 9.0.0 + EF Core Relational 10.0.0 NU1608 warning and it runs
Npgsql 9.0.4 + EF Core Relational 10.0.0 NU1608 + NU1107 error
Npgsql 9.0.4 + EF Core Relational 9.0.0 NU1605 error

Row two is the rule. Pomelo constrains a single package, so pinning above its range cannot raise a
version conflict — restore succeeds, the build succeeds, and the program runs, with a provider live
on the EF Core version it explicitly declares it does not support. I ran it. It printed 10.0.0.0.

So the rule shipped narrower than designed: it fires only where restore is permissive, and stays
quiet on the two shapes that already fail loudly. Repeating an error you have already seen is noise.

There is no table to maintain. Every provider declares its own range, so the constraint is read
out of the nuspec rather than asserted by us. Nothing about it is EF Core specific — it covers ASP.NET
Core integration libraries, analyzers tied to a compiler version, and test SDK and adapter pairs
without knowing they exist.

And it names the fix, because this failure is manufactured by one-package-per-pull-request
updaters and the repair has to be a single atomic change:

redecker check --online
Npgsql.EntityFrameworkCore.PostgreSQL 10.0.3 is the newest release that accepts
Microsoft.EntityFrameworkCore.Relational 10.0.4. Move both in one change: bumping either alone
leaves the pair broken.

The same evidence caught a bug in that suggestion. Asked which Pomelo release accepts EF Core
10.0.0, the first implementation answered 7.0.0 — truthfully, because Pomelo 7 declares an
unbounded minimum and so admits anything above it. Acting on it would downgrade a provider by two
majors to fix a version bump. Only newer releases are considered now, and when none work it says so
instead of inventing something.

RDK0012 — found by looking, not by waiting

Every rule until now came from a package somebody happened to notice, which biases the set towards
whatever broke recently in one person's build. This one came from asking the corpus.

Four cheap checks, run across 4,235 cached packages, with no rule written for any of them:

Candidate Packages Verdict
Stable package depends on a prerelease 22 (0.52%) shipped
lib/<tfm>/ with no assembly and no _._ 24 (0.57%) dropped — native and localisation packages
Package depends on itself 0 dropped
XML doc with no matching assembly 235 (5.55%) dropped — a convention, not a defect

The zero and the 5.55% are the valuable numbers. A check that never fires describes a problem that
does not exist; one firing on a twentieth of everything describes a convention. Both were worth
knowing before writing code, and both cost about a minute because the corpus was already on disk.

What survived is worth having. Opting into prereleases governs what you reference directly — not
what your dependencies reference. So a stable package with a prerelease dependency puts preview code
into a graph that opted out, and nothing announces it:

Package Depends on
Microsoft.Maui.Essentials@10.0.90 Xamarin.AndroidX.Security.SecurityCrypto 1.1.0.4-alpha07
Microsoft.Azure.Workflows.WebJobs.Extension@1.44.16 Microsoft.Azure.WebJobs.Script.Abstractions 1.0.0-preview
Microsoft.Windows.CsWin32@0.3.298 three prereleases

Microsoft.Maui.Essentials is the one to stare at: current, stable, installed everywhere, depending
on an alpha.

New: redecker check --online

check has always been offline and stays that way by default. --online adds the one rule that
must read nuspecs from nuget.org. Opt-in rather than automatic, because quietly reaching the network
in somebody's CI is a surprise nobody enjoys. Downloads share inspect's cache.

Also

Nuspec parsing is no longer regex. Dependencies are read with a real XML parser, matching on
local name so the schema namespace cannot break it. Tests pin the shapes a pattern would have missed:
reordered attributes, single quotes, newlines inside tags, no namespace, and dependencies both inside
<group> and flat beside it.

And a documented decision not to go further. Adopting NuGet.Packaging's PackageArchiveReader
was measured and declined for now: its
GetBuildItems() does not cover buildTransitive/, which RDK0006 checks, and it costs 985 KB of new
assemblies against a 379 KB tool — including NuGet.Configuration's settings machinery this tool has
no use for. Paying triple the payload to adopt an implementation that covers less ground is a bad
trade, and the reasoning is written down so it can be revisited.

Current state

Twelve rules. Across 2,682 Microsoft.* and System.* packages every rule fires between 0% and 0.8%,
and the evidence log records where each one
came from — including the checks that were measured and not turned into rules.

Full changelog: v0.3.0...v0.4.0


Release prepared by Claude (claude-opus-5), posting on behalf of @jzabroski.

v0.3.0 — two rules that had to earn it

Choose a tag to compare

@jzabroski jzabroski released this 31 Jul 23:04

Two new rules. Neither one shipped as designed, and that is the story of this release.

Both were proposed as ideas, measured against thousands of real packages first, and then rewritten
when the corpus disagreed. One of them was wrong three times before it was right.

Install

dotnet tool install --global dotnet-redecker    # investigate an upgrade
dotnet add package Redecker.MSBuild             # fail the build instead

📖 Documentation

New rules

Rule What it checks
RDK0009 incomplete symbol package — a .snupkg that covers some shipped assemblies but not others
RDK0010 framework folder mismatch — an assembly under lib/<framework>/ that a consumer targeting it could not load

RDK0010, and the two rules that didn't work

The idea is simple: lib/net45/ is a promise, and nothing verifies it. Restore reads the folder
name, picks the nearest match, and copies out whatever is inside without ever opening it. A net472
assembly under lib/net45/ restores cleanly and then buries the consumer in errors about types they
never touched.

The obvious implementation — compare the folder against the assembly's TargetFrameworkAttribute,
report any difference — produced 285 findings across 4,205 packages, and most of them were fine.
A netstandard2.0 assembly in lib/net8.0/ is not a mistake; it is the ordinary way to win
nearest-framework matching. A net45 build in lib/net452/ is a build reused rather than repeated.

The question that works is the one restore actually asks: can a project targeting this folder
consume this assembly?
NuGet already answers that, so Redecker asks it instead of re-deriving the
netstandard fallback chain and PCL profile contribution. 285 → 87.

Half of what remained sat in dead platforms, where MonoAndroid403 is an OS version, uap10.0
means .NETCore,Version=v5.0, and a PCL profile is a set intersection rather than a version. Not
wrong — unfixable, because the tooling that would republish those packages is gone. 87 → 42, in
22 packages.

Version of the rule Findings Useful
Compare the strings 285 mostly not
Ask NuGet about compatibility 87 yes, plus dead platforms
Scope to living frameworks 42 yes

What it catches is real, and a lot of it is Microsoft's own:

Package Folder Assembly targets
System.Security.Cryptography.OpenSsl@5.0.0 lib/net461/ .NETFramework,Version=v4.7
Microsoft.VisualStudio.TextTemplating.15.0 lib/net45/ .NETFramework,Version=v4.7.2
Microsoft.Web.Administration@11.1.0 lib/netstandard1.5/ .NETFramework,Version=v4.5
Microsoft.CodeCoverage@18.8.1 lib/net8.0/ .NETFramework,Version=v4.0
Microsoft.VisualStudio.DesignTools.Extensibility lib/net45/ .NETStandard,Version=v2.0

That last one is the trap running backwards. netstandard2.0 needs net461 or later, so a net45
project cannot load it either — being older is not automatically safe, which the compatibility
check gets right for free and a version comparison would have got wrong.

It is a warning rather than an error because Microsoft.CodeCoverage ships that .NET Framework 4.0
shim deliberately, and it is installed in a large share of the test projects in existence. Failing
those builds would destroy more work than it saved.

RDK0009 — incomplete symbol package

Publishing no symbols is a choice. 174 of 232 sampled packages make it, so the rule says nothing
there. Publishing some is almost always an accident: of the 58 that publish symbols, 57 cover
every assembly they ship
.

The single exception taught the rule its one exclusion. Microsoft.VisualStudio.Validation has 26
uncovered assemblies — all satellites, which correctly have no PDBs, because a resource assembly has
no code to step through. Written from the idea alone, that package would have been the rule's first
false positive.

Works offline on your own output, where a finding is still cheap to act on:

dotnet pack -c Release
redecker inspect --file ./artifacts/packages/Contoso.Widgets.1.0.0.nupkg

Surveys, as distinct from sweeps

A sweep runs the shipped rules and asks whether any of them has started lying. A survey runs
a measurement that is not a rule yet, to decide whether it should become one.

dotnet run --project tools/Redecker.Corpus -c Release -- survey-tfm results

Surveys read the download cache rather than the network, which makes them free to re-run — and that
turned out to matter more than it sounds. The target-framework survey was re-run twice because its
classifier was wrong, and being able to throw away an answer cheaply is what made it easy to admit.
Results are committed to
results/.

Fixed

RDK0006 false positives, both found by the top-2000 sweep: build logic reached only through a
property-valued import, and entry points supplied by MSBuild extension-point properties. The rule
went from 15 findings to 4 without losing a true one.

This one is worth naming, because RDK0006 had been retired outright at one
point
on the grounds that the corpus said it
misfired. That was the wrong call — testing shows the presence of bugs, not their absence, and a
rule that fires wrongly is usually a rule with a bug rather than a bad idea. It was restored and
then fixed.

Under the hood

RDK0010 reads PE metadata through System.Reflection.Metadata, which is in the shared framework
and, unlike Assembly.LoadFrom, neither runs code nor cares whether the host process could load the
assembly — reading a net40 image from a net8.0 tool has to work, because that mismatch is the
entire point.

New dependency: NuGet.Frameworks, for the compatibility question above. It is dependency-free
and it is the first step of what
issue #5 argues for more broadly: stop
hand-rolling what NuGet already implements correctly.

Current state

Ten rules. Across 2,682 Microsoft.* and System.* packages every rule fires between 0% and 0.8%,
and the evidence log records what each one
came from — including the rules that were not written, and why.

Full changelog: v0.2.0...v0.3.0


Release prepared by Claude (claude-opus-5), posting on behalf of @jzabroski.

v0.2.0 — eight rules, build-time checks, and a corpus that argues back

Choose a tag to compare

@jzabroski jzabroski released this 31 Jul 01:49

Restore only checks the maths. It never opens the box.

0.2.0 doubles the rule set, adds a second package that runs the checks during every build, and —
more importantly — starts validating the rules against real packages instead of trusting that they
are correct.

Install

dotnet tool install --global dotnet-redecker    # investigate an upgrade
dotnet add package Redecker.MSBuild             # fail the build instead

📖 Documentation

New: Redecker.MSBuild

Build-time checks, so a split package family is an error where somebody will see it rather than a
report they have to ask for. Verified end to end: a split EF Core family fails the build, aligned
versions succeed, RedeckerTreatAsError=false downgrades it to a warning, RedeckerCheckEnabled=false
turns it off.

The task targets netstandard2.0 so one asset serves both the .NET Framework MSBuild that Visual
Studio uses and the .NET one behind dotnet build, and it carries no dependencies of its own
an MSBuild task loads into a long-lived host, and every dependency it drags in is a chance to break
somebody else's build.

New rules

Rule What it checks
RDK0004 undocumented transitive pin — a declared version no project references, with no hint saying why
RDK0005 tool package not installable — missing DotnetToolSettings.xml, or an entry point that is not shipped
RDK0006 unimportable build file — build logic nothing inside the package imports
RDK0007 untracked output copy — copies into $(OutDir) that never reach @(FileWrites)
RDK0008 analyzer under a framework folder — the lib/ layout applied to analyzers/

RDK0004 is the one most repositories already trip. Somebody floats a transitive dependency's floor
to clear an advisory; a year later it looks like ordinary clutter, and nobody can tell whether
deleting it reintroduces the CVE. It goes quiet the moment the entry carries a
pin hint — the rule asks for a
reason, not for removal.

RDK0007 came from a report that Microsoft.Data.SqlClient struggles on net48. It does: the
defect is in Microsoft.Data.SqlClient.SNI, present identically in 5.2.0, 6.0.1 and 6.0.2, while
the main package and System.Data.SqlClient are clean.

New: check your package before it is permanent

dotnet pack -c Release
redecker inspect --file ./artifacts/packages/*.nupkg

RDK0001, RDK0005, RDK0006 and RDK0008 all describe defects that survive build, pack, restore
and publish — discovered only by whoever installs what you shipped, on a version nuget.org lets
you unlist but never delete. Redecker runs these against its own packages on every pull request.

Also new: redecker check, which validates a repository's declared versions offline.

Rules are now validated against real packages

tools/Redecker.Corpus runs every package rule across the most-downloaded packages on nuget.org.
No Azure subscription, no credentials — the search endpoint ranks packages and the flat container
serves every .nupkg over plain HTTP. Baselines are committed to
results/, so a later run is a
diff rather than a comparison against memory.

It paid for itself immediately, finding five false positives in rules that had already shipped:

  • Grpc.Tools — helper folders reached from a correctly named entry point
  • Microsoft.Graphics.Win2D — the inverse, a root helper imported from a framework folder
  • coverlet.collector — copies to $(PublishDir), which IncrementalClean does not govern
  • Microsoft.AspNetCore.Components.Analyzers — a path inside $([MSBuild]::NormalizePath(...))
  • Microsoft.VisualStudio.SlnGen.Tool — payload folders mistaken for tool assets

Current state across 2,680 Microsoft.* and System.* packages: every rule fires between 0% and
0.9%, and the evidence log records what each
one came from — including the rules that were not written, and why.

Also

Full changelog: v0.1.0...v0.2.0


Release prepared by Claude (claude-opus-5), posting on behalf of @jzabroski.