Transitive Software Composition Analysis (SCA)

Rule ND3121 Security

Raises an issue per NuGet package with known GitHub security advisories (https://github.com/advisories?query=+ecosystem%3Anuget) used by a package used by the application — hence transitive, unlike the rule Software Composition Analysis (SCA). The issue is reported on the first type (alphabetically) using a third-party assembly using the affected package.

Flagged Code
<!-- Acme.Sdk 2.1.0 depends on Newtonsoft.Json 12.0.1 (advisory GHSA-5crp-9r3c-p9vr) -->
<!-- Newtonsoft.Json is not referenced directly: the advisory is transitive -->
<ItemGroup>
   <PackageReference Include="Acme.Sdk" Version="2.1.0" />
</ItemGroup>
Fixed Code
<!-- Acme.Sdk 2.2.0 depends on Newtonsoft.Json 13.0.1, which has no known advisory -->
<ItemGroup>
   <PackageReference Include="Acme.Sdk" Version="2.2.0" />
</ItemGroup>
<!-- Alternative when no such Acme.Sdk version exists: pin the transitive package -->
<ItemGroup>
   <PackageReference Include="Acme.Sdk" Version="2.1.0" />
   <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>

How to fix

Update the package containing DirectAsm to a version not using a package with known security advisories.

CQLinq source

This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.

// <Name>Transitive Software Composition Analysis (SCA)</Name>
// <Id>ND3121:SoftwareCompositionAnalysisTransitive</Id>

warnif count > 0

// Use this hashset to avoid to have several SAC issues on a type
let hashset = new HashSet<IType>()
let thirdPartyAsmNames = ThirdParty.Assemblies.Select(a => a.Name).ToHashSetEx(StringComparer.OrdinalIgnoreCase)

// Avoid reporting packages reported by the non-transitive SAC rule
let directPackageNamesWithAdvisories = 
   (from asm in ThirdParty.Assemblies
    let advisories = asm.GetSecurityAdvisories()
    where advisories.Count > 0
    select (string)advisories.First().PackageName).ToHashSetEx(StringComparer.OrdinalIgnoreCase)

// Gather assembly references used by third-party assemblies 
// with one or several GitHub Security Advisories
let indirectAsmWithAdvisories = 
   from directAsm in ThirdParty.Assemblies
   
   // Assemblies from  Microsoft.NetCore.App.Runtime pkg reference version 
   // are always X.0.0.0 (like 8.0.0.0 or 9.0.0.0)
   // Thus this rule cannot detect if patched version is referenced.
   where !directAsm.Name.StartsWithAny("System.","Microsoft.")

   from indirectAsm in directAsm.AssemblyReferences
   where !indirectAsm.Name.StartsWithAny("System.","Microsoft.")

   // Don't match assemblies directly used by the app
   where !thirdPartyAsmNames.Contains(indirectAsm.Name) 

   let advisories = indirectAsm.GetSecurityAdvisories()
   where advisories.Count > 0
   let PackageName = (string)advisories.First().PackageName

   // Avoid reporting packages reported by the non-transitive SAC rule 
   where !directPackageNamesWithAdvisories.Contains(PackageName) 

   select new { 
     directAsm, 
     indirectAsm, 
     advisories,     
     PackageName
   } 

// Merge assemblies references within the same NuGet Package
from x in indirectAsmWithAdvisories.ToLookup(a1 => a1.PackageName)

// Gather data useful to describe the advisories and how to fix
let HighestSeverity = x.SelectMany(r => r.advisories).Max(ad => ad.Severity)
let AdvisoryWithHighestSeverity = x.SelectMany(r => r.advisories).First(ad => ad.Severity == HighestSeverity)
let ThirdPartyAssembliesImpacted = x.Select(x1 => x1.directAsm).Distinct().ToArray()
// Application AND test assemblies: a vulnerable package referenced only by tests is still a vulnerability
let AppAssembliesUser = ApplicationAndTestCode.Assemblies.UsingAny(ThirdPartyAssembliesImpacted)

// The issue appears on the first type using one culprit third-party assembly 
let typeUsers = ApplicationAndTestCode.Types.UsingAny(ThirdPartyAssembliesImpacted)
    .Where(t => !t.IsGeneratedByCompiler || t.Name.StartsWith("Program+")).ToArray()
let FirstAppTypeUser = (typeUsers.Length == 0 ? ApplicationAndTestCode.Types : typeUsers ) // Make sure at least one type else we get an exception!
    .Aggregate((minElement, nextElement) => 
        string.Compare(nextElement.Name, minElement.Name, StringComparison.Ordinal) < 0 && !hashset.Contains(nextElement)
        ? nextElement 
        : minElement)
let fillHashset = hashset.Add(FirstAppTypeUser)

select new { 
   FirstAppTypeUser,
   DirectAsm = x.First().directAsm,
   DirectAsmVersion = x.First().directAsm.Version,
   PackageName = x.Key,
   PackageVersion = x.First().indirectAsm.Version,
   ThirdPartyAssembliesImpacted,
   AppAssembliesUser,
   PatchAllAdvisoriesVersion = x.First().advisories.Max(ad => ad.PatchedVersion),
   HighestSeverity,
   AdvisoryWithHighestSeverity = AdvisoryWithHighestSeverity.Url,
   AdvisorySummary = AdvisoryWithHighestSeverity.Summary,
   NbAdvisory = x.First().advisories.Count,
   AdvisoriesUrl = x.First().advisories.Select(ad=> ad.Url).Except(AdvisoryWithHighestSeverity.Url).Aggregate(" ; "),
   Debt = 20.ToMinutes().ToDebt()
}

//<Expl>
// The application uses the **{1} version {2}** that uses the NuGet Package **{3} version {4}** which is affected by {11} Github security advisor{11?y:ies}.
//
// **Recommended Action:** Update the package of the *{1}* to a version that uses at least *{3}* version *{7}* or higher that has no known security advisory.
//
// Advisory with highest severity *{8}* is {9} : **{10}**. {11?:Other advisory url: }{12}
//
// The *{0}* is the first type (alphabetically) in the application that uses the *{1}*.
//</Expl>

//<Desc>
// Raises an issue per NuGet package with known GitHub security advisories
// (https://github.com/advisories?query=+ecosystem%3Anuget) used by a package
// used by the application — hence **transitive**, unlike the rule *Software
// Composition Analysis (SCA)*. The issue is reported on the first type
// (alphabetically) using a third-party assembly using the affected package.
//
// **Flagged Code**
// ```xml
// <!-- Acme.Sdk 2.1.0 depends on Newtonsoft.Json 12.0.1 (advisory GHSA-5crp-9r3c-p9vr) -->
// <!-- Newtonsoft.Json is not referenced directly: the advisory is transitive -->
// <ItemGroup>
//    <PackageReference Include="Acme.Sdk" Version="2.1.0" />
// </ItemGroup>
// ```
//
// **Fixed Code**
// ```xml
// <!-- Acme.Sdk 2.2.0 depends on Newtonsoft.Json 13.0.1, which has no known advisory -->
// <ItemGroup>
//    <PackageReference Include="Acme.Sdk" Version="2.2.0" />
// </ItemGroup>
// <!-- Alternative when no such Acme.Sdk version exists: pin the transitive package -->
// <ItemGroup>
//    <PackageReference Include="Acme.Sdk" Version="2.1.0" />
//    <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
// </ItemGroup>
// ```
//</Desc>

//<HowToFix>
// Update the package containing *DirectAsm* to a version not using a
// package with known security advisories.
//</HowToFix>

Security: all 39 items