From 5c6c6aa123f94c4dcf968eff4bc489ca8ddcb3d6 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 24 Apr 2025 13:11:01 -0400 Subject: [PATCH] Add support for detecting javascript assets in .NET projects using libman (#3825) * add support for .NET libman files Signed-off-by: Alex Goodman * fix when no libman detected Signed-off-by: Alex Goodman * add libman.json docs Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman --- syft/pkg/cataloger/dotnet/cataloger_test.go | 26 +++++ .../cataloger/dotnet/deps_binary_cataloger.go | 27 ++++- syft/pkg/cataloger/dotnet/deps_json.go | 5 +- syft/pkg/cataloger/dotnet/libman_json.go | 109 ++++++++++++++++++ .../image-net6-asp-libman/.dockerignore | 2 + .../image-net6-asp-libman/.gitignore | 2 + .../image-net6-asp-libman/Dockerfile | 13 +++ .../image-net6-asp-libman/src/.gitignore | 1 + .../src/LibManSample.csproj | 13 +++ .../image-net6-asp-libman/src/Program.cs | 24 ++++ .../image-net6-asp-libman/src/Startup.cs | 34 ++++++ .../image-net6-asp-libman/src/libman.json | 24 ++++ .../src/vendor/lodash.js | 1 + .../src/vendor/lodash.min.js | 1 + 14 files changed, 279 insertions(+), 3 deletions(-) create mode 100644 syft/pkg/cataloger/dotnet/libman_json.go create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/.dockerignore create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/.gitignore create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/Dockerfile create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/.gitignore create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/LibManSample.csproj create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/Program.cs create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/Startup.cs create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/libman.json create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/vendor/lodash.js create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/vendor/lodash.min.js diff --git a/syft/pkg/cataloger/dotnet/cataloger_test.go b/syft/pkg/cataloger/dotnet/cataloger_test.go index d4f638bb4..1e135318d 100644 --- a/syft/pkg/cataloger/dotnet/cataloger_test.go +++ b/syft/pkg/cataloger/dotnet/cataloger_test.go @@ -1008,6 +1008,18 @@ func TestCataloger(t *testing.T) { }, 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 { @@ -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 { t.Run(tt.name, func(t *testing.T) { diff --git a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go index c5964656f..4829ce7cf 100644 --- a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go +++ b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go @@ -78,6 +78,9 @@ func (c depsBinaryCataloger) Catalog(_ context.Context, resolver file.Resolver) var runtimePkgs []*pkg.Package for i := range pkgs { p := &pkgs[i] + if p.Type != pkg.DotnetPkg { + continue + } if isRuntime(p.Name) { existingRuntimeVersions.Add(p.Version) runtimePkgs = append(runtimePkgs, p) @@ -290,8 +293,22 @@ func packagesFromLogicalDepsJSON(doc logicalDepsJSON, config CatalogerConfig) (* 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. @@ -389,7 +406,13 @@ func findDepsJSON(resolver file.Resolver) ([]logicalDepsJSON, error, error) { 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 diff --git a/syft/pkg/cataloger/dotnet/deps_json.go b/syft/pkg/cataloger/dotnet/deps_json.go index 8388ddb66..70d8aa1be 100644 --- a/syft/pkg/cataloger/dotnet/deps_json.go +++ b/syft/pkg/cataloger/dotnet/deps_json.go @@ -9,6 +9,7 @@ import ( "github.com/scylladb/go-set/strset" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" ) type depsJSON struct { @@ -147,6 +148,7 @@ type logicalDepsJSON struct { PackagesByNameVersion map[string]logicalDepsJSONPackage PackageNameVersions *strset.Set BundlingDetected bool + LibmanPackages []pkg.Package } func (l logicalDepsJSON) RootPackage() (logicalDepsJSONPackage, bool) { @@ -196,7 +198,7 @@ var knownBundlers = strset.New( "Fody", // IL weaving framework ) -func getLogicalDepsJSON(deps depsJSON) logicalDepsJSON { +func getLogicalDepsJSON(deps depsJSON, lm *libmanJSON) logicalDepsJSON { packageMap := make(map[string]*logicalDepsJSONPackage) nameVersions := strset.New() @@ -244,6 +246,7 @@ func getLogicalDepsJSON(deps depsJSON) logicalDepsJSON { PackagesByNameVersion: packages, PackageNameVersions: nameVersions, BundlingDetected: bundlingDetected, + LibmanPackages: lm.packages(), } } diff --git a/syft/pkg/cataloger/dotnet/libman_json.go b/syft/pkg/cataloger/dotnet/libman_json.go new file mode 100644 index 000000000..4483a5cb4 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/libman_json.go @@ -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 +} diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/.dockerignore b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/.dockerignore new file mode 100644 index 000000000..585993c76 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/.dockerignore @@ -0,0 +1,2 @@ +.gitignore +Dockerfile \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/.gitignore b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/.gitignore new file mode 100644 index 000000000..b0b8376dc --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/.gitignore @@ -0,0 +1,2 @@ +/app +/extract.sh \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/Dockerfile b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/Dockerfile new file mode 100644 index 000000000..80bc9347f --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/Dockerfile @@ -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 . + + diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/.gitignore b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/.gitignore new file mode 100644 index 000000000..fbfccf63c --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/.gitignore @@ -0,0 +1 @@ +wwwroot/lib/ \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/LibManSample.csproj b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/LibManSample.csproj new file mode 100644 index 000000000..fccef1d18 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/LibManSample.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + + + + + + + + + diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/Program.cs b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/Program.cs new file mode 100644 index 000000000..243205ac6 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/Program.cs @@ -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(); + } +} diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/Startup.cs b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/Startup.cs new file mode 100644 index 000000000..b7c70b4e7 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/Startup.cs @@ -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!"); + }); + } + } +} diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/libman.json b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/libman.json new file mode 100644 index 000000000..2c95941b3 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/libman.json @@ -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/" + } + ] +} \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/vendor/lodash.js b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/vendor/lodash.js new file mode 100644 index 000000000..3352fb89b --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/vendor/lodash.js @@ -0,0 +1 @@ +// jk! nothing to see here! \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/vendor/lodash.min.js b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/vendor/lodash.min.js new file mode 100644 index 000000000..9ba7bbb31 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net6-asp-libman/src/vendor/lodash.min.js @@ -0,0 +1 @@ +// still nothing here... \ No newline at end of file