Add support for detecting javascript assets in .NET projects using libman (#3825)

* add support for .NET libman files

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix when no libman detected

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add libman.json docs

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2025-04-24 13:11:01 -04:00 committed by GitHub
parent 43a85dfb85
commit 5c6c6aa123
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 279 additions and 3 deletions

View File

@ -1008,6 +1008,18 @@ func TestCataloger(t *testing.T) {
}, },
assertion: assertAccurateNetRuntimePackage, assertion: assertAccurateNetRuntimePackage,
}, },
{
name: "libman support",
fixture: "image-net6-asp-libman",
cataloger: NewDotnetDepsBinaryCataloger(DefaultCatalogerConfig()),
expectedPkgs: []string{
"LibManSample @ 1.0.0 (/app/LibManSample.deps.json)",
"jquery @ 3.3.1 (/app/libman.json)",
},
expectedRels: []string{
"jquery @ 3.3.1 (/app/libman.json) [dependency-of] LibManSample @ 1.0.0 (/app/LibManSample.deps.json)",
},
},
} }
for _, tt := range cases { for _, tt := range cases {
@ -1134,6 +1146,20 @@ func TestDotnetDepsCataloger_regressions(t *testing.T) {
}, },
), ),
}, },
{
name: "libman support",
fixture: "image-net6-asp-libman",
cataloger: NewDotnetDepsBinaryCataloger(DefaultCatalogerConfig()),
assertion: assertPackages(
[]string{
"jquery", // a javascript package, not the nuget package
},
[]string{
"vendor", // this is the string reference for a filesystem provider
"lodash", // this is from a filesystem provider, which is not supported
},
),
},
} }
for _, tt := range cases { for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View File

@ -78,6 +78,9 @@ func (c depsBinaryCataloger) Catalog(_ context.Context, resolver file.Resolver)
var runtimePkgs []*pkg.Package var runtimePkgs []*pkg.Package
for i := range pkgs { for i := range pkgs {
p := &pkgs[i] p := &pkgs[i]
if p.Type != pkg.DotnetPkg {
continue
}
if isRuntime(p.Name) { if isRuntime(p.Name) {
existingRuntimeVersions.Add(p.Version) existingRuntimeVersions.Add(p.Version)
runtimePkgs = append(runtimePkgs, p) runtimePkgs = append(runtimePkgs, p)
@ -290,8 +293,22 @@ func packagesFromLogicalDepsJSON(doc logicalDepsJSON, config CatalogerConfig) (*
pkgMap[nameVersion] = *dotnetPkg pkgMap[nameVersion] = *dotnetPkg
} }
} }
rels := relationshipsFromLogicalDepsJSON(doc, pkgMap, skippedDepPkgs)
return rootPkg, pkgs, relationshipsFromLogicalDepsJSON(doc, pkgMap, skippedDepPkgs) // ensure that any libman packages are associated with the all root packages
for _, libmanPkg := range doc.LibmanPackages {
pkgs = append(pkgs, libmanPkg)
if rootPkg == nil {
continue
}
rels = append(rels, artifact.Relationship{
From: libmanPkg,
To: *rootPkg,
Type: artifact.DependencyOfRelationship,
})
}
return rootPkg, pkgs, rels
} }
// relationshipsFromLogicalDepsJSON creates relationships from a logicalDepsJSON document for only the given syft packages. // relationshipsFromLogicalDepsJSON creates relationships from a logicalDepsJSON document for only the given syft packages.
@ -389,7 +406,13 @@ func findDepsJSON(resolver file.Resolver) ([]logicalDepsJSON, error, error) {
continue continue
} }
depsJSONs = append(depsJSONs, getLogicalDepsJSON(*dj)) libman, err := findLibmanJSON(resolver, loc)
if err != nil {
unknownErr = unknown.Append(unknownErr, loc, err)
libman = nil
}
depsJSONs = append(depsJSONs, getLogicalDepsJSON(*dj, libman))
} }
return depsJSONs, unknownErr, nil return depsJSONs, unknownErr, nil

View File

@ -9,6 +9,7 @@ import (
"github.com/scylladb/go-set/strset" "github.com/scylladb/go-set/strset"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
) )
type depsJSON struct { type depsJSON struct {
@ -147,6 +148,7 @@ type logicalDepsJSON struct {
PackagesByNameVersion map[string]logicalDepsJSONPackage PackagesByNameVersion map[string]logicalDepsJSONPackage
PackageNameVersions *strset.Set PackageNameVersions *strset.Set
BundlingDetected bool BundlingDetected bool
LibmanPackages []pkg.Package
} }
func (l logicalDepsJSON) RootPackage() (logicalDepsJSONPackage, bool) { func (l logicalDepsJSON) RootPackage() (logicalDepsJSONPackage, bool) {
@ -196,7 +198,7 @@ var knownBundlers = strset.New(
"Fody", // IL weaving framework "Fody", // IL weaving framework
) )
func getLogicalDepsJSON(deps depsJSON) logicalDepsJSON { func getLogicalDepsJSON(deps depsJSON, lm *libmanJSON) logicalDepsJSON {
packageMap := make(map[string]*logicalDepsJSONPackage) packageMap := make(map[string]*logicalDepsJSONPackage)
nameVersions := strset.New() nameVersions := strset.New()
@ -244,6 +246,7 @@ func getLogicalDepsJSON(deps depsJSON) logicalDepsJSON {
PackagesByNameVersion: packages, PackagesByNameVersion: packages,
PackageNameVersions: nameVersions, PackageNameVersions: nameVersions,
BundlingDetected: bundlingDetected, BundlingDetected: bundlingDetected,
LibmanPackages: lm.packages(),
} }
} }

View File

@ -0,0 +1,109 @@
package dotnet
import (
"encoding/json"
"path"
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
// libmanJSON represents the libman.json file format in ASP.NET projects for describing javascript assets to be downloaded and bundled
// see https://github.com/aspnet/LibraryManager/wiki/libman.json-reference
type libmanJSON struct {
Location file.Location `json:"-"`
Version string `json:"version"`
DefaultProvider string `json:"defaultProvider"`
Libraries []struct {
Library string `json:"library"`
Files []string `json:"files"`
Destination string `json:"destination"`
Provider string `json:"provider,omitempty"`
} `json:"libraries"`
}
func (l *libmanJSON) packages() []pkg.Package {
if l == nil {
return nil
}
var pkgs []pkg.Package
for _, lib := range l.Libraries {
if lib.Provider == "filesystem" {
// there is no name and version with filesystem providers
continue
}
fields := strings.Split(lib.Library, "@")
if len(fields) != 2 {
continue
}
name := fields[0]
version := fields[1]
p := pkg.Package{
Name: name,
Version: version,
Locations: file.NewLocationSet(l.Location),
Type: pkg.NpmPkg,
PURL: packageurl.NewPackageURL(
packageurl.TypeNPM,
"",
name,
version,
nil,
"",
).ToString(),
Language: pkg.JavaScript,
}
p.SetID()
pkgs = append(pkgs, p)
}
return pkgs
}
func newLibmanJSON(reader file.LocationReadCloser) (*libmanJSON, error) {
var doc libmanJSON
dec := json.NewDecoder(reader)
if err := dec.Decode(&doc); err != nil {
return nil, err
}
for i := range doc.Libraries {
l := &doc.Libraries[i]
if l.Provider == "" {
l.Provider = doc.DefaultProvider
}
}
doc.Location = reader.Location
return &doc, nil
}
func findLibmanJSON(resolver file.Resolver, depsJSON file.Location) (*libmanJSON, error) {
parent := path.Dir(depsJSON.RealPath)
loc := resolver.RelativeFileByPath(depsJSON, path.Join(parent, "libman.json"))
if loc == nil {
return nil, nil
}
reader, err := resolver.FileContentsByLocation(*loc)
defer internal.CloseAndLogError(reader, loc.RealPath)
if err != nil {
return nil, err
}
internal.CloseAndLogError(reader, loc.RealPath)
lj, err := newLibmanJSON(file.NewLocationReadCloser(*loc, reader))
if err != nil {
return nil, err
}
return lj, nil
}

View File

@ -0,0 +1,2 @@
.gitignore
Dockerfile

View File

@ -0,0 +1,2 @@
/app
/extract.sh

View File

@ -0,0 +1,13 @@
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
ARG RUNTIME=win-x64
COPY . .
WORKDIR /src
RUN dotnet restore -r $RUNTIME
RUN dotnet publish -c Release --no-restore -o /app
FROM busybox:latest
WORKDIR /app
COPY --from=build /app .

View File

@ -0,0 +1 @@
wwwroot/lib/

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<!-- A PackageReference to Microsoft.AspNetCore.App is not necessary when targeting .NET Core 3.0 or higher. If Microsoft.NET.Sdk.Web is used, the shared framework will be referenced automatically (or override with FrameworkReference) -->
<!-- <PackageReference Include="Microsoft.AspNetCore.App" /> -->
<PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="2.1.175" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace LibManSample
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace LibManSample
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
}
}

View File

@ -0,0 +1,24 @@
{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "jquery@3.3.1",
"files": [
"jquery.min.js",
"jquery.js",
"jquery.min.map"
],
"destination": "wwwroot/lib/jquery/"
},
{
"provider": "filesystem",
"library": "vendor",
"files": [
"lodash.js",
"lodash.min.js"
],
"destination": "wwwroot/lib/lodash/"
}
]
}

View File

@ -0,0 +1 @@
// jk! nothing to see here!

View File

@ -0,0 +1 @@
// still nothing here...