Merge the .NET deps.json and PE binary catalogers (#3563)

* add combined deps.json + pe binary cataloger

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

* deprecate pe and deps standalone catalogers

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

* parse resource names + add tests

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

* fix integration and CLI tests

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

* add some helpful code comments

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

* allow for dropping Dep packages that are missing DLLs

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

* migrate json schema changes to 24

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

* keep application configuration

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

* correct config help

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

* [wip] detect claims of dlls within deps.json

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

* [wip] fix tests

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

* add assembly repack detection

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

* .net package count is lower due to dll claim requirement

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-03-27 14:38:16 -04:00 committed by GitHub
parent 4a9437808e
commit ad9928cb2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 9068 additions and 1176 deletions

View File

@ -86,8 +86,10 @@ def main(file_path: str | None):
show("The following paths are missing or have no content, but have corresponding .fingerprint files:")
for path in sorted(missing_content):
show(f"- {path}")
show("Please ensure these paths exist and have content if they are directories.")
exit(1)
# when adding new cache directories there is a time where it is not possible to have this directory without
# running the tests first... but this step is a prerequisite for running the tests. We should not block on this.
# show("Please ensure these paths exist and have content if they are directories.")
# exit(1)
sha256_hash = calculate_sha256(fingerprint_contents)

View File

@ -168,7 +168,9 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
return pkgcataloging.Config{
Binary: binary.DefaultClassifierCatalogerConfig(),
Dotnet: dotnet.DefaultCatalogerConfig().
WithCertificateValidation(cfg.Dotnet.EnableCertificateValidation),
WithDepPackagesMustHaveDLL(cfg.Dotnet.DepPackagesMustHaveDLL).
WithDepPackagesMustClaimDLL(cfg.Dotnet.DepPackagesMustClaimDLL).
WithRelaxDLLClaimsWhenBundlingDetected(cfg.Dotnet.RelaxDLLClaimsWhenBundlingDetected),
Golang: golang.DefaultCatalogerConfig().
WithSearchLocalModCacheLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.SearchLocalModCacheLicenses)).
WithLocalModCacheDir(cfg.Golang.LocalModCacheDir).

View File

@ -6,7 +6,11 @@ import (
)
type dotnetConfig struct {
EnableCertificateValidation bool `json:"enable-certificate-validation" yaml:"enable-certificate-validation" mapstructure:"enable-certificate-validation"`
DepPackagesMustHaveDLL bool `mapstructure:"dep-packages-must-have-dll" json:"dep-packages-must-have-dll" yaml:"dep-packages-must-have-dll"`
DepPackagesMustClaimDLL bool `mapstructure:"dep-packages-must-claim-dll" json:"dep-packages-must-claim-dll" yaml:"dep-packages-must-claim-dll"`
RelaxDLLClaimsWhenBundlingDetected bool `mapstructure:"relax-dll-claims-when-bundling-detected" json:"relax-dll-claims-when-bundling-detected" yaml:"relax-dll-claims-when-bundling-detected"`
}
var _ interface {
@ -14,12 +18,16 @@ var _ interface {
} = (*dotnetConfig)(nil)
func (o *dotnetConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.EnableCertificateValidation, `enable certificate validation -- this requires an active internet connection to download certificates and CRLs`)
descriptions.Add(&o.DepPackagesMustHaveDLL, `only keep dep.json packages which an executable on disk can be found for`)
descriptions.Add(&o.DepPackagesMustClaimDLL, `only keep dep.json packages which have a runtime/resource DLL claimed in the deps.json targets section (but not necessarily found on disk)`)
descriptions.Add(&o.RelaxDLLClaimsWhenBundlingDetected, `show all packages from the deps.json if bundling tooling is present as a dependency (e.g. ILRepack)`)
}
func defaultDotnetConfig() dotnetConfig {
def := dotnet.DefaultCatalogerConfig()
return dotnetConfig{
EnableCertificateValidation: def.EnableCertificateValidation,
DepPackagesMustHaveDLL: def.DepPackagesMustHaveDLL,
DepPackagesMustClaimDLL: def.DepPackagesMustClaimDLL,
RelaxDLLClaimsWhenBundlingDetected: def.RelaxDLLClaimsWhenBundlingDetected,
}
}

View File

@ -78,11 +78,27 @@ var imageOnlyTestCases = []testCase{
},
},
{
name: "find dot net executable",
name: "find .NET packages (deps.json + .dlls)",
pkgType: pkg.DotnetPkg,
pkgLanguage: pkg.Dotnet,
pkgInfo: map[string]string{
// executable
"DocuSign.eSign": "6.8.0.0",
// deps.json
"AWSSDK.Core": "3.7.10.6",
"Microsoft.Extensions.DependencyInjection": "6.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
"Microsoft.Extensions.Logging": "6.0.0",
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
"Microsoft.Extensions.Options": "6.0.0",
"Microsoft.Extensions.Primitives": "6.0.0",
"Newtonsoft.Json": "13.0.1",
"Serilog": "2.10.0",
"Serilog.Sinks.Console": "4.0.1",
//"System.Diagnostics.DiagnosticSource": "6.0.0", // no dll claims in deps.json targets section
//"System.Runtime.CompilerServices.Unsafe": "6.0.0", // no dll claims in deps.json targets section
"TestCommon": "1.0.0",
"TestLibrary": "1.0.0",
},
},
}
@ -241,10 +257,11 @@ var dirOnlyTestCases = []testCase{
},
},
{
name: "find dotnet packages",
name: "find dotnet packages (.deps.json)",
pkgType: pkg.DotnetPkg,
pkgLanguage: pkg.Dotnet,
pkgInfo: map[string]string{
// all from deps.json
"AWSSDK.Core": "3.7.10.6",
"Microsoft.Extensions.DependencyInjection": "6.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
@ -255,10 +272,10 @@ var dirOnlyTestCases = []testCase{
"Newtonsoft.Json": "13.0.1",
"Serilog": "2.10.0",
"Serilog.Sinks.Console": "4.0.1",
"System.Diagnostics.DiagnosticSource": "6.0.0",
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
"TestCommon": "1.0.0",
"TestLibrary": "1.0.0",
//"System.Diagnostics.DiagnosticSource": "6.0.0", // no dll claims in deps.json targets section
//"System.Runtime.CompilerServices.Unsafe": "6.0.0", // no dll claims in deps.json targets section
"TestCommon": "1.0.0",
"TestLibrary": "1.0.0",
},
},
{

View File

@ -52,8 +52,16 @@ func TestAllPackageCatalogersReachableInTasks(t *testing.T) {
assert.Equal(t, len(taskTagsByName), constructorCount, "mismatch in number of cataloger constructors and task names")
exceptions := strset.New(
// not reachable since they are deprecated
"dotnet-portable-executable-cataloger",
"dotnet-deps-cataloger",
// not reachable by design
"sbom-cataloger",
)
for taskName, tags := range taskTagsByName {
if taskName == "sbom-cataloger" {
if exceptions.Has(taskName) {
continue // this is a special case
}
if !strset.New(tags...).HasAny(pkgcataloging.ImageTag, pkgcataloging.DirectoryTag) {

3
go.mod
View File

@ -65,7 +65,6 @@ require (
github.com/pelletier/go-toml v1.9.5
github.com/quasilyte/go-ruleguard/dsl v0.3.22
github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c
github.com/saferwall/pe v1.5.6
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
github.com/sanity-io/litter v1.5.8
github.com/sassoftware/go-rpmutils v0.4.0
@ -141,7 +140,6 @@ require (
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/fgprof v0.9.5 // indirect
@ -209,7 +207,6 @@ require (
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect

7
go.sum
View File

@ -279,8 +279,6 @@ github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj6
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/elliotchance/phpserialize v1.4.0 h1:cAp/9+KSnEbUC8oYCE32n2n84BeW8HOY3HMDI8hG2OY=
@ -725,8 +723,6 @@ github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c h1:8
github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c/go.mod h1:kwM/7r/rVluTE8qJbHAffduuqmSv4knVQT2IajGvSiA=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/saferwall/pe v1.5.6 h1:DrRLnoQFxHWJ5lJUmrH7X2L0xeUu6SUS95Dc61eW2Yc=
github.com/saferwall/pe v1.5.6/go.mod h1:mJx+PuptmNpoPFBNhWs/uDMFL/kTHVZIkg0d4OUJFbQ=
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
@ -743,8 +739,6 @@ github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd7
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY=
github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d h1:RQqyEogx5J6wPdoxqL132b100j8KjcVHO1c0KLRoIhc=
github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d/go.mod h1:PegD7EVqlN88z7TpCqH92hHP+GBpfomGCCnw1PFtNOA=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
@ -1109,7 +1103,6 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@ -3,5 +3,5 @@ package internal
const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "16.0.23"
JSONSchemaVersion = "16.0.24"
)

View File

@ -76,7 +76,6 @@ func DefaultPackageTaskFactories() Factories {
// language-specific package declared catalogers ///////////////////////////////////////////////////////////////////////////
newSimplePackageTaskFactory(cpp.NewConanCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "cpp", "conan"),
newSimplePackageTaskFactory(dart.NewPubspecLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
newSimplePackageTaskFactory(dotnet.NewDotnetDepsCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dotnet", "c#"),
newSimplePackageTaskFactory(elixir.NewMixLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "elixir"),
newSimplePackageTaskFactory(erlang.NewRebarLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "erlang"),
newSimplePackageTaskFactory(erlang.NewOTPCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "erlang", "otp"),
@ -117,11 +116,13 @@ func DefaultPackageTaskFactories() Factories {
newSimplePackageTaskFactory(ocaml.NewOpamPackageManagerCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "ocaml", "opam"),
// language-specific package for both image and directory scans (but not necessarily declared) ////////////////////////////////////////
newSimplePackageTaskFactory(dotnet.NewDotnetPackagesLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.ImageTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dotnet", "c#"),
newPackageTaskFactory(
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
return dotnet.NewDotnetPortableExecutableCataloger(cfg.PackagesConfig.Dotnet)
}, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "dotnet", "c#", "binary"),
return dotnet.NewDotnetDepsBinaryCataloger(cfg.PackagesConfig.Dotnet)
},
pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dotnet", "c#",
),
newSimplePackageTaskFactory(dotnet.NewDotnetPackagesLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.ImageTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dotnet", "c#"),
newSimplePackageTaskFactory(python.NewInstalledPackageCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "python"),
newPackageTaskFactory(
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
@ -160,5 +161,10 @@ func DefaultPackageTaskFactories() Factories {
newSimplePackageTaskFactory(bitnamiSbomCataloger.NewCataloger, "bitnami", pkgcataloging.InstalledTag, pkgcataloging.ImageTag),
newSimplePackageTaskFactory(wordpress.NewWordpressPluginCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "wordpress"),
newSimplePackageTaskFactory(terraform.NewLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "terraform"),
// deprecated catalogers ////////////////////////////////////////
// these are catalogers that should not be selectable other than specific inclusion via name or "deprecated" tag (to remain backwards compatible)
newSimplePackageTaskFactory(dotnet.NewDotnetDepsCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
newSimplePackageTaskFactory(dotnet.NewDotnetPortableExecutableCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.0.23/document",
"$id": "anchore.io/schema/syft/json/16.0.24/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@ -519,6 +519,14 @@
},
"hashPath": {
"type": "string"
},
"executables": {
"patternProperties": {
".*": {
"$ref": "#/$defs/DotnetPortableExecutableEntry"
}
},
"type": "object"
}
},
"type": "object",

View File

@ -21,4 +21,7 @@ const (
// LanguageTag should be used to identify catalogers that cataloging language-specific packages.
LanguageTag = "language"
// DeprecatedTag should be used to identify catalogers that are deprecated.
DeprecatedTag = "deprecated"
)

View File

@ -126,13 +126,13 @@ func (c *CreateSBOMConfig) WithDataGenerationConfig(cfg cataloging.DataGeneratio
return c
}
// WithPackagesConfig allows for defining any specific behavior for syft-implemented catalogers.
// WithPackagesConfig allows for defining any specific package cataloging behavior for syft-implemented catalogers.
func (c *CreateSBOMConfig) WithPackagesConfig(cfg pkgcataloging.Config) *CreateSBOMConfig {
c.Packages = cfg
return c
}
// WithPackagesConfig allows for defining any specific behavior for syft-implemented catalogers.
// WithLicenseConfig allows for defining any specific license cataloging behavior for syft-implemented catalogers.
func (c *CreateSBOMConfig) WithLicenseConfig(cfg cataloging.LicenseConfig) *CreateSBOMConfig {
c.Licenses = cfg
return c
@ -278,6 +278,10 @@ func (c *CreateSBOMConfig) selectTasks(src source.Description) ([]task.Task, []t
return nil, nil, nil, err
}
if deprecatedNames := deprecatedTasks(finalTaskGroups); len(deprecatedNames) > 0 {
log.WithFields("catalogers", strings.Join(deprecatedNames, ", ")).Warn("deprecated catalogers are being used (please remove them from your configuration)")
}
finalPkgTasks := finalTaskGroups[0]
finalFileTasks := finalTaskGroups[1]
@ -309,6 +313,18 @@ func (c *CreateSBOMConfig) selectTasks(src source.Description) ([]task.Task, []t
return finalPkgTasks, finalFileTasks, &selection, nil
}
func deprecatedTasks(taskGroups [][]task.Task) []string {
// we want to identify any deprecated catalogers that are being used but default selections will always additionally select `file`
// catalogers. For this reason, we must explicitly remove `file` catalogers in the selection request. This means if we
// deprecate a file cataloger we will need special processing.
_, selection, err := task.SelectInGroups(taskGroups, cataloging.SelectionRequest{DefaultNamesOrTags: []string{pkgcataloging.DeprecatedTag}, RemoveNamesOrTags: []string{filecataloging.FileTag}})
if err != nil {
// ignore the error, as it is not critical
return nil
}
return selection.Result.List()
}
func logTaskNames(tasks []task.Task, kind string) {
// log as tree output (like tree command)
log.Debugf("selected %d %s tasks", len(tasks), kind)

View File

@ -22,6 +22,12 @@ var knownNonMetadataTypeNames = strset.New(
"LicenseSet",
)
// these are names that would be removed due to common convention (e.g. used within another metadata type) but are
// known to be metadata types themselves. Adding to this list will prevent the removal of the type from the schema.
var knownMetadaTypeNames = strset.New(
"DotnetPortableExecutableEntry",
)
func DiscoverTypeNames() ([]string, error) {
root, err := RepoRoot()
if err != nil {
@ -66,7 +72,8 @@ func findMetadataDefinitionNames(paths ...string) ([]string, error) {
}
// any definition that is used within another struct should not be considered a top-level metadata definition
names.Remove(usedNames.List()...)
removeNames := strset.Difference(usedNames, knownMetadaTypeNames)
names.Remove(removeNames.List()...)
// remove known exceptions, that is, types exported in the pkg Package that are not used
// in a metadata type but are not metadata types themselves.

View File

@ -23,6 +23,7 @@ func main() {
panic(fmt.Errorf("unable to get all metadata type names: %w", err))
}
// useful for debugging...
// for _, typeName := range typeNames {
// fmt.Printf(" - %s\n", typeName)
//}

View File

@ -0,0 +1,39 @@
package dotnet
import (
"context"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
// binary cataloger will search for .dll and .exe files and create packages based off of the version resources embedded
// as a resource directory within the executable. If there is no evidence of a .NET runtime (a CLR header) then no
// package will be created.
// Deprecated: use depsBinaryCataloger instead which combines the PE and deps.json data which yields more accurate results (will be removed in syft v2.0).
type binaryCataloger struct {
}
func (c binaryCataloger) Name() string {
return "dotnet-portable-executable-cataloger"
}
func (c binaryCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
var unknowns error
peFiles, ldpeUnknownErr, err := findPEFiles(resolver)
if err != nil {
return nil, nil, err
}
if ldpeUnknownErr != nil {
unknowns = unknown.Join(unknowns, ldpeUnknownErr)
}
var pkgs []pkg.Package
for _, pe := range peFiles {
pkgs = append(pkgs, newDotnetBinaryPackage(pe.VersionResources, pe.Location))
}
return pkgs, nil, unknowns
}

View File

@ -1,6 +1,3 @@
/*
Package dotnet provides a concrete Cataloger implementation relating to packages within the C#/.NET language/runtime ecosystem.
*/
package dotnet
import (
@ -8,19 +5,27 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
// NewDotnetDepsCataloger returns a new Dotnet cataloger object base on deps json files.
// NewDotnetDepsBinaryCataloger returns a cataloger based on PE and deps.json file contents.
func NewDotnetDepsBinaryCataloger(config CatalogerConfig) pkg.Cataloger {
return &depsBinaryCataloger{
config: config,
}
}
// NewDotnetDepsCataloger returns a cataloger based on deps.json file contents.
// Deprecated: use NewDotnetDepsBinaryCataloger instead which combines the PE and deps.json data which yields more accurate results (will be removed in syft v2.0).
func NewDotnetDepsCataloger() pkg.Cataloger {
return generic.NewCataloger("dotnet-deps-cataloger").
WithParserByGlobs(parseDotnetDeps, "**/*.deps.json")
return &depsCataloger{}
}
// NewDotnetPortableExecutableCataloger returns a new Dotnet cataloger object base on portable executable files.
func NewDotnetPortableExecutableCataloger(cfg CatalogerConfig) pkg.Cataloger {
p := dotnetPortableExecutableParser{cfg: cfg}
return generic.NewCataloger("dotnet-portable-executable-cataloger").
WithParserByGlobs(p.parseDotnetPortableExecutable, "**/*.dll", "**/*.exe")
// NewDotnetPortableExecutableCataloger returns a cataloger based on PE file contents.
// Deprecated: use NewDotnetDepsBinaryCataloger instead which combines the PE and deps.json data which yields more accurate results (will be removed in syft v2.0).
func NewDotnetPortableExecutableCataloger() pkg.Cataloger {
return &binaryCataloger{}
}
// NewDotnetPackagesLockCataloger returns a cataloger based on packages.lock.json files.
func NewDotnetPackagesLockCataloger() pkg.Cataloger {
return generic.NewCataloger("dotnet-packages-lock-cataloger").WithParserByGlobs(parseDotnetPackagesLock, "**/packages.lock.json")
return generic.NewCataloger("dotnet-packages-lock-cataloger").
WithParserByGlobs(parseDotnetPackagesLock, "**/packages.lock.json")
}

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,37 @@
package dotnet
type CatalogerConfig struct {
EnableCertificateValidation bool `json:"enable-certificate-validation" yaml:"enable-certificate-validation" mapstructure:"enable-certificate-validation"`
// DepPackagesMustHaveDLL allows for deps.json packages to be included only if there is a DLL on disk for that package.
DepPackagesMustHaveDLL bool `mapstructure:"dep-packages-must-have-dll" json:"dep-packages-must-have-dll" yaml:"dep-packages-must-have-dll"`
// DepPackagesMustClaimDLL allows for deps.json packages to be included only if there is a runtime/resource DLL claimed in the deps.json targets section.
// This does not require such claimed DLLs to exist on disk. The behavior of this
DepPackagesMustClaimDLL bool `mapstructure:"dep-packages-must-claim-dll" json:"dep-packages-must-claim-dll" yaml:"dep-packages-must-claim-dll"`
// RelaxDLLClaimsWhenBundlingDetected will look for indications of IL bundle tooling via deps.json package names
// and, if found (and this config option is enabled), will relax the DepPackagesMustClaimDLL value to `false` only in those cases.
RelaxDLLClaimsWhenBundlingDetected bool `mapstructure:"relax-dll-claims-when-bundling-detected" json:"relax-dll-claims-when-bundling-detected" yaml:"relax-dll-claims-when-bundling-detected"`
}
func (c CatalogerConfig) WithCertificateValidation(enable bool) CatalogerConfig {
c.EnableCertificateValidation = enable
func (c CatalogerConfig) WithDepPackagesMustHaveDLL(requireDlls bool) CatalogerConfig {
c.DepPackagesMustHaveDLL = requireDlls
return c
}
func (c CatalogerConfig) WithDepPackagesMustClaimDLL(requireDlls bool) CatalogerConfig {
c.DepPackagesMustClaimDLL = requireDlls
return c
}
func (c CatalogerConfig) WithRelaxDLLClaimsWhenBundlingDetected(relax bool) CatalogerConfig {
c.RelaxDLLClaimsWhenBundlingDetected = relax
return c
}
func DefaultCatalogerConfig() CatalogerConfig {
return CatalogerConfig{}
return CatalogerConfig{
DepPackagesMustHaveDLL: false,
DepPackagesMustClaimDLL: true,
RelaxDLLClaimsWhenBundlingDetected: true,
}
}

View File

@ -0,0 +1,396 @@
package dotnet
import (
"context"
"fmt"
"path"
"regexp"
"sort"
"strings"
"github.com/scylladb/go-set/strset"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/relationship"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
const (
depsJSONGlob = "**/*.deps.json"
dllGlob = "**/*.dll"
exeGlob = "**/*.exe"
)
// depsBinaryCataloger will search for both deps.json evidence and PE file evidence to create packages. All packages
// from both sources are raised up, but with one merge operation applied; If a deps.json package reference can be
// correlated with a PE file, the PE file is attached to the package as supporting evidence.
type depsBinaryCataloger struct {
config CatalogerConfig
}
func (c depsBinaryCataloger) Name() string {
return "dotnet-deps-binary-cataloger"
}
func (c depsBinaryCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
depJSONDocs, unknowns, err := findDepsJSON(resolver)
if err != nil {
return nil, nil, err
}
peFiles, ldpeUnknownErr, err := findPEFiles(resolver)
if err != nil {
return nil, nil, err
}
if ldpeUnknownErr != nil {
unknowns = unknown.Join(unknowns, ldpeUnknownErr)
}
// partition the logical PE files by location and pair them with the logicalDepsJSON
pairedDepsJSONs, remainingPeFiles, remainingDepsJSONs := partitionPEs(depJSONDocs, peFiles)
var pkgs []pkg.Package
var relationships []artifact.Relationship
depDocGroups := [][]logicalDepsJSON{pairedDepsJSONs}
if !c.config.DepPackagesMustHaveDLL {
depDocGroups = append(depDocGroups, remainingDepsJSONs)
}
for _, docs := range depDocGroups {
for _, doc := range docs {
ps, rs := packagesFromLogicalDepsJSON(doc, c.config)
pkgs = append(pkgs, ps...)
relationships = append(relationships, rs...)
}
}
for _, pe := range remainingPeFiles {
pkgs = append(pkgs, newDotnetBinaryPackage(pe.VersionResources, pe.Location))
}
return pkgs, relationships, unknowns
}
// partitionPEs pairs PE files with the deps.json based on directory containment.
func partitionPEs(depJsons []logicalDepsJSON, peFiles []logicalPE) ([]logicalDepsJSON, []logicalPE, []logicalDepsJSON) {
// sort deps.json paths from longest to shortest. This is so we are processing the most specific match first.
sort.Slice(depJsons, func(i, j int) bool {
return len(depJsons[i].Location.RealPath) > len(depJsons[j].Location.RealPath)
})
peFilesByPath := make(map[file.Coordinates][]logicalPE)
var remainingPeFiles []logicalPE
for _, pe := range peFiles {
var found bool
for i := range depJsons {
dep := &depJsons[i]
if isParentOf(dep.Location.RealPath, pe.Location.RealPath) && attachAssociatedExecutables(dep, pe) {
peFilesByPath[dep.Location.Coordinates] = append(peFilesByPath[dep.Location.Coordinates], pe)
found = true
// note: we cannot break from the dep JSON search since the same binary could be associated with multiple packages
// across multiple deps.json files.
}
}
if !found {
remainingPeFiles = append(remainingPeFiles, pe)
}
}
var pairedDepsJSON []logicalDepsJSON
var remainingDepsJSON []logicalDepsJSON
for _, dep := range depJsons {
if _, ok := peFilesByPath[dep.Location.Coordinates]; !ok {
remainingDepsJSON = append(remainingDepsJSON, dep)
} else {
pairedDepsJSON = append(pairedDepsJSON, dep)
}
}
return pairedDepsJSON, remainingPeFiles, remainingDepsJSON
}
// attachAssociatedExecutables looks for PE files matching runtime or resource entries
// and attaches them to the appropriate package.
func attachAssociatedExecutables(dep *logicalDepsJSON, pe logicalPE) bool {
appDir := path.Dir(dep.Location.RealPath)
relativeDllPath := strings.TrimPrefix(strings.TrimPrefix(pe.Location.RealPath, appDir), "/")
var found bool
for key, p := range dep.PackagesByNameVersion {
if targetPath, ok := p.RuntimePathsByRelativeDLLPath[relativeDllPath]; ok {
pe.TargetPath = targetPath
p.Executables = append(p.Executables, pe)
dep.PackagesByNameVersion[key] = p // update the map with the modified package
found = true
continue
}
if targetPath, ok := p.ResourcePathsByRelativeDLLPath[relativeDllPath]; ok {
pe.TargetPath = targetPath
p.Executables = append(p.Executables, pe)
dep.PackagesByNameVersion[key] = p // update the map with the modified package
found = true
}
}
return found
}
var libPrefixPattern = regexp.MustCompile(`^lib/net[^/]+/`)
// trimLibPrefix removes prefixes like "lib/net6.0/" from a path.
func trimLibPrefix(s string) string {
if match := libPrefixPattern.FindString(s); match != "" {
parts := strings.Split(s, "/")
if len(parts) > 2 {
return strings.Join(parts[2:], "/")
}
}
return s
}
// isParentOf checks if parentFile's directory is a prefix of childFile's directory.
func isParentOf(parentFile, childFile string) bool {
parentDir := path.Dir(parentFile)
childDir := path.Dir(childFile)
return strings.HasPrefix(childDir, parentDir)
}
// packagesFromDepsJSON creates packages from a list of logicalDepsJSON documents.
func packagesFromDepsJSON(docs []logicalDepsJSON, config CatalogerConfig) ([]pkg.Package, []artifact.Relationship) {
var pkgs []pkg.Package
var relationships []artifact.Relationship
for _, ldj := range docs {
ps, rs := packagesFromLogicalDepsJSON(ldj, config)
pkgs = append(pkgs, ps...)
relationships = append(relationships, rs...)
}
return pkgs, relationships
}
// packagesFromLogicalDepsJSON converts a logicalDepsJSON (using the new map type) into catalog packages.
func packagesFromLogicalDepsJSON(doc logicalDepsJSON, config CatalogerConfig) ([]pkg.Package, []artifact.Relationship) {
var rootPkg *pkg.Package
if rootLpkg, hasRoot := doc.RootPackage(); !hasRoot {
rootPkg = newDotnetDepsPackage(rootLpkg, doc.Location)
}
var pkgs []pkg.Package
pkgMap := make(map[string]pkg.Package)
if rootPkg != nil {
pkgs = append(pkgs, *rootPkg)
pkgMap[createNameAndVersion(rootPkg.Name, rootPkg.Version)] = *rootPkg
}
nameVersions := doc.PackageNameVersions.List()
sort.Strings(nameVersions)
// process each non-root package
skippedDepPkgs := make(map[string]logicalDepsJSONPackage)
for _, nameVersion := range nameVersions {
name, version := extractNameAndVersion(nameVersion)
if rootPkg != nil && name == rootPkg.Name && version == rootPkg.Version {
continue
}
lp := doc.PackagesByNameVersion[nameVersion]
if config.DepPackagesMustHaveDLL && len(lp.Executables) == 0 {
// could not find a paired DLL and the user required this...
skippedDepPkgs[nameVersion] = lp
continue
}
claimsDLLs := len(lp.RuntimePathsByRelativeDLLPath) > 0 || len(lp.ResourcePathsByRelativeDLLPath) > 0
if config.DepPackagesMustClaimDLL && !claimsDLLs {
if config.RelaxDLLClaimsWhenBundlingDetected && !doc.BundlingDetected || !config.RelaxDLLClaimsWhenBundlingDetected {
// could not find a runtime or resource path and the user required this...
// and there is no evidence of a bundler in the dependencies (e.g. ILRepack)
skippedDepPkgs[nameVersion] = lp
continue
}
}
dotnetPkg := newDotnetDepsPackage(lp, doc.Location)
if dotnetPkg != nil {
pkgs = append(pkgs, *dotnetPkg)
pkgMap[nameVersion] = *dotnetPkg
}
}
return pkgs, relationshipsFromLogicalDepsJSON(doc, pkgMap, skippedDepPkgs)
}
// relationshipsFromLogicalDepsJSON creates relationships from a logicalDepsJSON document for only the given syft packages.
// It is possible that the document describes more packages than that is provided as syft packages, in which cases
// those relationships will not be created. If there are any skipped packages, we still want to logically represent
// dependency relationships, jumping over the skipped packages.
func relationshipsFromLogicalDepsJSON(doc logicalDepsJSON, pkgMap map[string]pkg.Package, skipped map[string]logicalDepsJSONPackage) []artifact.Relationship {
var relationships []artifact.Relationship
for _, lp := range doc.PackagesByNameVersion {
if lp.Targets == nil {
continue
}
for depName, depVersion := range lp.Targets.Dependencies {
depNameVersion := createNameAndVersion(depName, depVersion)
thisPkg, ok := pkgMap[lp.NameVersion]
if !ok {
continue
}
var depPkgs []pkg.Package
depPkg, ok := pkgMap[depNameVersion]
if !ok {
skippedDepPkg, ok := skipped[depNameVersion]
if !ok {
// this package wasn't explicitly skipped, so it could be a malformed deps.json file
// ignore this case and do not create a relationships
continue
}
// we have a skipped package, so we need to create a relationship but looking a the nearest
// package with an associated PE file for even dependency listed on the skipped package.
// Take note that the skipped depedency's dependency could also be skipped, so we need to
// do this recursively.
depPkgs = findNearestDependencyPackages(skippedDepPkg, pkgMap, skipped, strset.New())
} else {
depPkgs = append(depPkgs, depPkg)
}
for _, d := range depPkgs {
rel := artifact.Relationship{
From: d,
To: thisPkg,
Type: artifact.DependencyOfRelationship,
}
relationships = append(relationships, rel)
}
}
}
relationship.Sort(relationships)
return relationships
}
func findNearestDependencyPackages(skippedDep logicalDepsJSONPackage, pkgMap map[string]pkg.Package, skipped map[string]logicalDepsJSONPackage, processed *strset.Set) []pkg.Package {
var nearestPkgs []pkg.Package
// if we have already processed this package, skip it to avoid infinite recursion
if processed.Has(skippedDep.NameVersion) {
return nearestPkgs
}
processed.Add(skippedDep.NameVersion)
for depName, depVersion := range skippedDep.Targets.Dependencies {
depNameVersion := createNameAndVersion(depName, depVersion)
depPkg, ok := pkgMap[depNameVersion]
if !ok {
skippedDepPkg, ok := skipped[depNameVersion]
if !ok {
// this package wasn't explicitly skipped, so it could be a malformed deps.json file
// ignore this case and do not create a relationships
continue
}
nearestPkgs = append(nearestPkgs, findNearestDependencyPackages(skippedDepPkg, pkgMap, skipped, processed)...)
} else {
nearestPkgs = append(nearestPkgs, depPkg)
}
}
return nearestPkgs
}
// findDepsJSON locates and parses all deps.json files.
func findDepsJSON(resolver file.Resolver) ([]logicalDepsJSON, error, error) {
locs, err := resolver.FilesByGlob(depsJSONGlob)
if err != nil {
return nil, nil, fmt.Errorf("unable to find deps.json files: %w", err)
}
var depsJSONs []logicalDepsJSON
var unknownErr error
for _, loc := range locs {
dj, err := readDepsJSON(resolver, loc)
if err != nil {
unknownErr = unknown.Append(unknownErr, loc, err)
continue
}
depsJSONs = append(depsJSONs, getLogicalDepsJSON(*dj))
}
return depsJSONs, unknownErr, nil
}
// readDepsJSON reads and parses a single deps.json file.
func readDepsJSON(resolver file.Resolver, loc file.Location) (*depsJSON, error) {
reader, err := resolver.FileContentsByLocation(loc)
if err != nil {
return nil, unknown.New(loc, fmt.Errorf("unable to read deps.json file: %w", err))
}
defer internal.CloseAndLogError(reader, loc.RealPath)
dj, err := newDepsJSON(file.NewLocationReadCloser(loc, reader))
if err != nil {
return nil, unknown.New(loc, fmt.Errorf("unable to parse deps.json file: %w", err))
}
if dj == nil {
return nil, unknown.New(loc, fmt.Errorf("expected to find packages in deps.json but did not: %q", loc.RealPath))
}
return dj, nil
}
// findPEFiles locates and parses all PE files (dll/exe).
func findPEFiles(resolver file.Resolver) ([]logicalPE, error, error) {
peLocs, err := resolver.FilesByGlob(dllGlob, exeGlob)
if err != nil {
return nil, nil, fmt.Errorf("unable to find PE files: %w", err)
}
var peFiles []logicalPE
var unknownErr error
for _, loc := range peLocs {
ldpe, err := readPEFile(resolver, loc)
if err != nil {
unknownErr = unknown.Append(unknownErr, loc, err)
continue
}
if ldpe == nil {
continue
}
peFiles = append(peFiles, *ldpe)
}
return peFiles, unknownErr, nil
}
// readPEFile reads and parses a single PE file.
func readPEFile(resolver file.Resolver, loc file.Location) (*logicalPE, error) {
reader, err := resolver.FileContentsByLocation(loc)
if err != nil {
return nil, unknown.New(loc, fmt.Errorf("unable to read PE file: %w", err))
}
defer internal.CloseAndLogError(reader, loc.RealPath)
ldpe, err := getLogicalDotnetPE(file.NewLocationReadCloser(loc, reader))
if err != nil {
return nil, unknown.New(loc, fmt.Errorf("unable to parse PE file: %w", err))
}
if ldpe == nil {
return nil, nil
}
if !ldpe.CLR.hasEvidenceOfCLR() {
// this is not a .NET binary
return nil, nil
}
return ldpe, nil
}

View File

@ -0,0 +1,31 @@
package dotnet
import (
"context"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
// depsCataloger will search for deps.json file contents.
// Deprecated: use depsBinaryCataloger instead which combines the PE and deps.json data which yields more accurate results (will be removed in syft v2.0).
type depsCataloger struct {
}
func (c depsCataloger) Name() string {
return "dotnet-deps-cataloger"
}
func (c depsCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
depJSONDocs, unknowns, err := findDepsJSON(resolver)
if err != nil {
return nil, nil, err
}
pkgs, rels := packagesFromDepsJSON(depJSONDocs, CatalogerConfig{
DepPackagesMustHaveDLL: false,
DepPackagesMustClaimDLL: false,
})
return pkgs, rels, unknowns
}

View File

@ -0,0 +1,168 @@
package dotnet
import (
"encoding/json"
"fmt"
"strings"
"github.com/scylladb/go-set/strset"
"github.com/anchore/syft/syft/file"
)
type depsJSON struct {
Location file.Location
RuntimeTarget runtimeTarget `json:"runtimeTarget"`
Targets map[string]map[string]depsTarget `json:"targets"`
Libraries map[string]depsLibrary `json:"libraries"`
}
type runtimeTarget struct {
Name string `json:"name"`
}
type depsTarget struct {
Dependencies map[string]string `json:"dependencies"`
Runtime map[string]map[string]string `json:"runtime"`
Resources map[string]map[string]string `json:"resources"`
}
type depsLibrary struct {
Type string `json:"type"`
Path string `json:"path"`
Sha512 string `json:"sha512"`
HashPath string `json:"hashPath"`
}
// logicalDepsJSONPackage merges target and library information for a given package from all dep.json entries.
// Note: this is not a real construct of the deps.json, just a useful reorganization of the data for downstream processing.
type logicalDepsJSONPackage struct {
NameVersion string
Targets *depsTarget
Library *depsLibrary
// RuntimePathsByRelativeDLLPath is a map of the relative path to the DLL relative to the deps.json file
// to the target path as described in the deps.json target entry under "runtime".
RuntimePathsByRelativeDLLPath map[string]string
// ResourcePathsByRelativeDLLPath is a map of the relative path to the DLL relative to the deps.json file
// to the target path as described in the deps.json target entry under "resource".
ResourcePathsByRelativeDLLPath map[string]string
// Executables is a list of all the executables that are part of this package. This is populated by the PE cataloger
// and not something that is found in the deps.json file. This allows us to associate the PE files with this package
// based on the relative path to the DLL.
Executables []logicalPE
}
type logicalDepsJSON struct {
Location file.Location
RuntimeTarget runtimeTarget
PackagesByNameVersion map[string]logicalDepsJSONPackage
PackageNameVersions *strset.Set
BundlingDetected bool
}
func (l logicalDepsJSON) RootPackage() (logicalDepsJSONPackage, bool) {
rootName := getDepsJSONFilePrefix(l.Location.RealPath)
if rootName == "" {
return logicalDepsJSONPackage{}, false
}
// iterate over the map to find the root package. If we don't find the root package, that's ok! We still want to
// get all of the packages that are defined in this deps.json file.
for _, p := range l.PackagesByNameVersion {
name, _ := extractNameAndVersion(p.NameVersion)
// there can be multiple projects defined in a deps.json and only by convention is the root project the same name as the deps.json file
// however there are other configurations that can lead to differences (e.g. "tool_fsc" vs "fsc.deps.json").
if p.Library != nil && p.Library.Type == "project" && name == rootName {
return p, true
}
}
return logicalDepsJSONPackage{}, false
}
func newDepsJSON(reader file.LocationReadCloser) (*depsJSON, error) {
var doc depsJSON
dec := json.NewDecoder(reader)
if err := dec.Decode(&doc); err != nil {
return nil, fmt.Errorf("failed to parse deps.json file: %w", err)
}
doc.Location = reader.Location
return &doc, nil
}
var knownBundlers = strset.New(
"ILRepack.Lib.MSBuild.Task", // The most official use of ILRepack https://github.com/gluck/il-repack
"ILRepack.Lib", // library interface for ILRepack
"ILRepack.Lib.MSBuild", // uses Cecil 0.10
"ILRepack.Lib.NET", // uses ModuleDefinitions instead of filenames
"ILRepack.NETStandard", // .NET Standard compatible version
"ILRepack.FullAuto", // https://github.com/kekyo/ILRepack.FullAuto
"ILMerge", // deprecated, but still used in some projects https://github.com/dotnet/ILMerge
"JetBrains.Build.ILRepack", // generally from https://www.nuget.org/packages?q=ilrepack&sortBy=relevance
// other bundling/modification tools found in results
"PostSharp.Community.Packer", // Embeds dependencies as resources
"Brokenevent.ILStrip", // assembly cleaner (removes unused parts)
"Brokenevent.ILStrip.CLI", // command-line/MSBuild variant
"Costura.Fody", // referenced in MSBuildRazorCompiler.Lib
"Fody", // IL weaving framework
)
func getLogicalDepsJSON(deps depsJSON) logicalDepsJSON {
packageMap := make(map[string]*logicalDepsJSONPackage)
nameVersions := strset.New()
for _, targets := range deps.Targets {
for libName, target := range targets {
_, exists := packageMap[libName]
if !exists {
var lib *depsLibrary
l, ok := deps.Libraries[libName]
if ok {
lib = &l
}
runtimePaths := make(map[string]string)
for path := range target.Runtime {
runtimePaths[trimLibPrefix(path)] = path
}
resourcePaths := make(map[string]string)
for path := range target.Resources {
trimmedPath := trimLibPrefix(path)
if _, exists := resourcePaths[trimmedPath]; exists {
continue
}
resourcePaths[trimmedPath] = path
}
p := &logicalDepsJSONPackage{
NameVersion: libName,
Library: lib,
Targets: &target,
RuntimePathsByRelativeDLLPath: runtimePaths,
ResourcePathsByRelativeDLLPath: resourcePaths,
}
packageMap[libName] = p
nameVersions.Add(libName)
}
}
}
packages := make(map[string]logicalDepsJSONPackage)
var bundlingDetected bool
for _, p := range packageMap {
name := strings.Split(p.NameVersion, "/")[0]
if !bundlingDetected && knownBundlers.Has(name) {
bundlingDetected = true
}
packages[p.NameVersion] = *p
}
return logicalDepsJSON{
Location: deps.Location,
RuntimeTarget: deps.RuntimeTarget,
PackagesByNameVersion: packages,
PackageNameVersions: nameVersions,
BundlingDetected: bundlingDetected,
}
}

View File

@ -5,26 +5,36 @@ import (
"regexp"
"strings"
"github.com/anchore/go-version"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
func newDotnetDepsPackage(nameVersion string, lib dotnetDepsLibrary, locations ...file.Location) *pkg.Package {
name, version := extractNameAndVersion(nameVersion)
var (
// spaceRegex includes nbsp (#160) considered to be a space character
spaceRegex = regexp.MustCompile(`[\s\xa0]+`)
numberRegex = regexp.MustCompile(`\d`)
versionPunctuationRegex = regexp.MustCompile(`[.,]+`)
)
m := pkg.DotnetDepsEntry{
Name: name,
Version: version,
Path: lib.Path,
Sha512: lib.Sha512,
HashPath: lib.HashPath,
// newDotnetDepsPackage creates a new Dotnet dependency package from a logicalDepsJSONPackage.
// Note that the new logicalDepsJSONPackage now directly holds library and executable information.
func newDotnetDepsPackage(lp logicalDepsJSONPackage, depsLocation file.Location) *pkg.Package {
name, ver := extractNameAndVersion(lp.NameVersion)
locs := file.NewLocationSet(depsLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
for _, pe := range lp.Executables {
locs.Add(pe.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation))
}
m := newDotnetDepsEntry(lp)
p := &pkg.Package{
Name: name,
Version: version,
Locations: file.NewLocationSet(locations...),
Version: ver,
Locations: locs,
PURL: packageURL(m),
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
@ -36,6 +46,66 @@ func newDotnetDepsPackage(nameVersion string, lib dotnetDepsLibrary, locations .
return p
}
// newDotnetDepsEntry creates a Dotnet dependency entry using the new logicalDepsJSONPackage.
func newDotnetDepsEntry(lp logicalDepsJSONPackage) pkg.DotnetDepsEntry {
name, ver := extractNameAndVersion(lp.NameVersion)
// since this is a metadata type, we should not allocate this collection unless there are entries; otherwise
// the JSON serialization will produce an empty object instead of omitting the field.
var pes map[string]pkg.DotnetPortableExecutableEntry
if len(lp.Executables) > 0 {
pes = make(map[string]pkg.DotnetPortableExecutableEntry)
for _, pe := range lp.Executables {
pes[pe.TargetPath] = newDotnetPortableExecutableEntry(pe)
}
}
var path, sha, hashPath string
lib := lp.Library
if lib != nil {
path = lib.Path
sha = lib.Sha512
hashPath = lib.HashPath
}
return pkg.DotnetDepsEntry{
Name: name,
Version: ver,
Path: path,
Sha512: sha,
HashPath: hashPath,
Executables: pes,
}
}
// newDotnetPortableExecutableEntry creates a portable executable entry from a logicalPE.
func newDotnetPortableExecutableEntry(pe logicalPE) pkg.DotnetPortableExecutableEntry {
return newDotnetPortableExecutableEntryFromMap(pe.VersionResources)
}
func newDotnetPortableExecutableEntryFromMap(vr map[string]string) pkg.DotnetPortableExecutableEntry {
return pkg.DotnetPortableExecutableEntry{
// for some reason, the assembly version is sometimes stored as "Assembly Version" and sometimes as "AssemblyVersion"
AssemblyVersion: cleanVersionResourceField(vr["Assembly Version"], vr["AssemblyVersion"]),
LegalCopyright: cleanVersionResourceField(vr["LegalCopyright"]),
Comments: cleanVersionResourceField(vr["Comments"]),
InternalName: cleanVersionResourceField(vr["InternalName"]),
CompanyName: cleanVersionResourceField(vr["CompanyName"]),
ProductName: cleanVersionResourceField(vr["ProductName"]),
ProductVersion: cleanVersionResourceField(vr["ProductVersion"]),
}
}
func cleanVersionResourceField(values ...string) string {
for _, value := range values {
if value == "" {
continue
}
return strings.TrimSpace(value)
}
return ""
}
func getDepsJSONFilePrefix(p string) string {
r := regexp.MustCompile(`([^\\\/]+)\.deps\.json$`)
match := r.FindStringSubmatch(p)
@ -48,7 +118,9 @@ func getDepsJSONFilePrefix(p string) string {
func extractNameAndVersion(nameVersion string) (name, version string) {
fields := strings.Split(nameVersion, "/")
name = fields[0]
version = fields[1]
if len(fields) > 1 {
version = fields[1]
}
return
}
@ -61,16 +133,8 @@ func packageURL(m pkg.DotnetDepsEntry) string {
var qualifiers packageurl.Qualifiers
return packageurl.NewPackageURL(
// This originally was packageurl.TypeDotnet, but this isn't a valid PURL type, according to:
// https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst
// Some history:
// https://github.com/anchore/packageurl-go/pull/8 added the type to Anchore's fork
// due to this PR: https://github.com/anchore/syft/pull/951
// There were questions about "dotnet" being the right purlType at the time, but it was
// acknowledged that scanning a dotnet file does not necessarily mean the packages found
// are nuget packages and so the alternate type was added. Since this is still an invalid
// PURL type, however, we will use TypeNuget and revisit at such time there is a better
// official PURL type available.
// Although we use TypeNuget here due to historical reasons, note that it does not necessarily
// mean the package is a NuGet package.
packageurl.TypeNuget,
"",
m.Name,
@ -79,3 +143,155 @@ func packageURL(m pkg.DotnetDepsEntry) string {
"",
).ToString()
}
func newDotnetBinaryPackage(versionResources map[string]string, f file.Location) pkg.Package {
name := findNameFromVersionResources(versionResources)
ver := findVersionFromVersionResources(versionResources)
metadata := newDotnetPortableExecutableEntryFromMap(versionResources)
p := pkg.Package{
Name: name,
Version: ver,
Locations: file.NewLocationSet(f.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
Type: pkg.DotnetPkg,
Language: pkg.Dotnet,
PURL: binaryPackageURL(name, ver),
Metadata: metadata,
}
p.SetID()
return p
}
func binaryPackageURL(name, version string) string {
if name == "" {
return ""
}
return packageurl.NewPackageURL(
packageurl.TypeNuget,
"",
name,
version,
nil,
"",
).ToString()
}
func findNameFromVersionResources(versionResources map[string]string) string {
// PE files not authored by Microsoft tend to use ProductName as an identifier.
nameFields := []string{"ProductName", "FileDescription", "InternalName", "OriginalFilename"}
if isMicrosoftVersionResource(versionResources) {
// For Microsoft files, prioritize FileDescription.
nameFields = []string{"FileDescription", "InternalName", "OriginalFilename", "ProductName"}
}
for _, field := range nameFields {
value := spaceNormalize(versionResources[field])
if value == "" {
continue
}
return value
}
return ""
}
func isMicrosoftVersionResource(versionResources map[string]string) bool {
return strings.Contains(strings.ToLower(versionResources["CompanyName"]), "microsoft") ||
strings.Contains(strings.ToLower(versionResources["ProductName"]), "microsoft")
}
// spaceNormalize trims and normalizes whitespace in a string.
func spaceNormalize(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
// Ensure valid UTF-8.
value = strings.ToValidUTF8(value, "")
// Consolidate all whitespace.
value = spaceRegex.ReplaceAllString(value, " ")
// Remove non-printable characters.
value = regexp.MustCompile(`[\x00-\x1f]`).ReplaceAllString(value, "")
// Consolidate again and trim.
value = spaceRegex.ReplaceAllString(value, " ")
value = strings.TrimSpace(value)
return value
}
func findVersionFromVersionResources(versionResources map[string]string) string {
productVersion := extractVersionFromResourcesValue(versionResources["ProductVersion"])
fileVersion := extractVersionFromResourcesValue(versionResources["FileVersion"])
semanticVersionCompareResult := keepGreaterSemanticVersion(productVersion, fileVersion)
if semanticVersionCompareResult != "" {
return semanticVersionCompareResult
}
productVersionDetail := punctuationCount(productVersion)
fileVersionDetail := punctuationCount(fileVersion)
if containsNumber(productVersion) && productVersionDetail >= fileVersionDetail {
return productVersion
}
if containsNumber(fileVersion) && fileVersionDetail > 0 {
return fileVersion
}
if containsNumber(productVersion) {
return productVersion
}
if containsNumber(fileVersion) {
return fileVersion
}
return productVersion
}
func extractVersionFromResourcesValue(version string) string {
version = strings.TrimSpace(version)
out := ""
for i, f := range strings.Fields(version) {
if containsNumber(out) && !containsNumber(f) {
return out
}
if i == 0 {
out = f
} else {
out += " " + f
}
}
return out
}
func keepGreaterSemanticVersion(productVersion string, fileVersion string) string {
semanticProductVersion, err := version.NewVersion(productVersion)
if err != nil || semanticProductVersion == nil {
log.Tracef("Unable to create semantic version from product version %s", productVersion)
return ""
}
semanticFileVersion, err := version.NewVersion(fileVersion)
if err != nil || semanticFileVersion == nil {
log.Tracef("Unable to create semantic version from file version %s", fileVersion)
return productVersion
}
if semanticProductVersion.Equal(semanticFileVersion) {
return ""
}
if semanticFileVersion.GreaterThan(semanticProductVersion) {
return fileVersion
}
return productVersion
}
func containsNumber(s string) bool {
return numberRegex.MatchString(s)
}
func punctuationCount(s string) int {
return len(versionPunctuationRegex.FindAllString(s, -1))
}

View File

@ -4,6 +4,10 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func Test_getDepsJSONFilePrefix(t *testing.T) {
@ -39,3 +43,287 @@ func Test_getDepsJSONFilePrefix(t *testing.T) {
})
}
}
func Test_NewDotnetBinaryPackage(t *testing.T) {
tests := []struct {
name string
versionResources map[string]string
expectedPackage pkg.Package
}{
{
name: "dotnet package with extra version info",
versionResources: map[string]string{
"InternalName": "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll",
"FileVersion": "3.14.40721.0918 xxxfffdddjjjj",
"FileDescription": "Active Directory Authentication Library",
"ProductName": "Active Directory Authentication Library",
"Comments": "",
"CompanyName": "Microsoft Corporation",
"LegalTrademarks": "",
"LegalCopyright": "Copyright (c) Microsoft Corporation. All rights reserved.",
"OriginalFilename": "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll",
"ProductVersion": "c61f043686a544863efc014114c42e844f905336",
"Assembly Version": "3.14.2.11",
},
expectedPackage: pkg.Package{
Name: "Active Directory Authentication Library",
Version: "3.14.40721.0918",
Metadata: pkg.DotnetPortableExecutableEntry{
AssemblyVersion: "3.14.2.11",
LegalCopyright: "Copyright (c) Microsoft Corporation. All rights reserved.",
InternalName: "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll",
CompanyName: "Microsoft Corporation",
ProductName: "Active Directory Authentication Library",
ProductVersion: "c61f043686a544863efc014114c42e844f905336",
},
},
},
{
// show we can do a best effort to make a package from bad data
name: "dotnet package with malformed field and extended version",
versionResources: map[string]string{
"CompanyName": "Microsoft Corporation",
"FileDescription": "äbFile\xa0\xa1Versi on",
"FileVersion": "4.6.25512.01 built by: dlab-DDVSOWINAGE016. Commit Hash: d0d5c7b49271cadb6d97de26d8e623e98abdc8db",
"InternalName": "äbFileVersion",
"LegalCopyright": "© Microsoft Corporation. All rights reserved.",
"OriginalFilename": "TProductName",
"ProductName": "Microsoft® .NET Framework",
"ProductVersion": "4.6.25512.01 built by: dlab-DDVSOWINAGE016. Commit Hash: d0d5c7b49271cadb6d97de26d8e623e98abdc8db",
},
expectedPackage: pkg.Package{
Name: "äbFileVersi on",
Version: "4.6.25512.01",
PURL: "pkg:nuget/%C3%A4bFileVersi%20on@4.6.25512.01",
Metadata: pkg.DotnetPortableExecutableEntry{
LegalCopyright: "© Microsoft Corporation. All rights reserved.",
InternalName: "äb\x01FileVersion",
CompanyName: "Microsoft Corporation",
ProductName: "Microsoft® .NET Framework",
ProductVersion: "4.6.25512.01 built by: dlab-DDVSOWINAGE016. Commit Hash: d0d5c7b49271cadb6d97de26d8e623e98abdc8db",
},
},
},
{
name: "System.Data.Linq.dll",
versionResources: map[string]string{
"CompanyName": "Microsoft Corporation",
"FileDescription": "System.Data.Linq.dll",
"FileVersion": "4.7.3190.0 built by: NET472REL1LAST_C",
"InternalName": "System.Data.Linq.dll",
"LegalCopyright": "© Microsoft Corporation. All rights reserved.",
"OriginalFilename": "System.Data.Linq.dll",
"ProductName": "Microsoft® .NET Framework",
"ProductVersion": "4.7.3190.0",
},
expectedPackage: pkg.Package{
Name: "System.Data.Linq.dll",
Version: "4.7.3190.0",
},
},
{
name: "curl",
versionResources: map[string]string{
"CompanyName": "curl, https://curl.se/",
"FileDescription": "The curl executable",
"FileVersion": "8.4.0",
"InternalName": "curl",
"LegalCopyright": "© Daniel Stenberg, <daniel@haxx.se>.",
"OriginalFilename": "curl.exe",
"ProductName": "The curl executable",
"ProductVersion": "8.4.0",
},
expectedPackage: pkg.Package{
Name: "The curl executable",
Version: "8.4.0",
},
},
{
name: "Prometheus",
versionResources: map[string]string{
"AssemblyVersion": "8.0.0.0",
"CompanyName": "",
"FileDescription": "",
"FileVersion": "8.0.1",
"InternalName": "Prometheus.AspNetCore.dll",
"OriginalFilename": "Prometheus.AspNetCore.dll",
"ProductName": "",
"ProductVersion": "8.0.1",
},
expectedPackage: pkg.Package{
Name: "Prometheus.AspNetCore.dll",
Version: "8.0.1",
},
},
{
name: "Hidden Input",
versionResources: map[string]string{
"FileDescription": "Reads from stdin without leaking info to the terminal and outputs back to stdout",
"FileVersion": "1, 0, 0, 0",
"InternalName": "hiddeninput",
"LegalCopyright": "Jordi Boggiano - 2012",
"OriginalFilename": "hiddeninput.exe",
"ProductName": "Hidden Input",
"ProductVersion": "1, 0, 0, 0",
},
expectedPackage: pkg.Package{
Name: "Hidden Input",
Version: "1, 0, 0, 0",
},
},
{
name: "SQLite3",
versionResources: map[string]string{
"CompanyName": "SQLite Development Team",
"FileDescription": "SQLite is a software library that implements a self-contained, serverless, zero-configuration, transactional SQL database engine.",
"FileVersion": "3.23.2",
"InternalName": "sqlite3",
"LegalCopyright": "http://www.sqlite.org/copyright.html",
"ProductName": "SQLite",
"ProductVersion": "3.23.2",
},
expectedPackage: pkg.Package{
Name: "SQLite",
Version: "3.23.2",
},
},
{
name: "Brave Browser",
versionResources: map[string]string{
"CompanyName": "Brave Software, Inc.",
"FileDescription": "Brave Browser",
"FileVersion": "80.1.7.92",
"InternalName": "chrome_exe",
"LegalCopyright": "Copyright 2016 The Brave Authors. All rights reserved.",
"OriginalFilename": "chrome.exe",
"ProductName": "Brave Browser",
"ProductVersion": "80.1.7.92",
},
expectedPackage: pkg.Package{
Name: "Brave Browser",
Version: "80.1.7.92",
},
},
{
name: "Better product version",
versionResources: map[string]string{
"FileDescription": "Better version",
"FileVersion": "80.1.7",
"ProductVersion": "80.1.7.92",
},
expectedPackage: pkg.Package{
Name: "Better version",
Version: "80.1.7.92",
},
},
{
name: "Better file version",
versionResources: map[string]string{
"FileDescription": "Better version",
"FileVersion": "80.1.7.92",
"ProductVersion": "80.1.7",
},
expectedPackage: pkg.Package{
Name: "Better version",
Version: "80.1.7.92",
},
},
{
name: "Higher semantic version Product Version",
versionResources: map[string]string{
"FileDescription": "Higher semantic version Product Version",
"FileVersion": "3.0.0.0",
"ProductVersion": "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
expectedPackage: pkg.Package{
Name: "Higher semantic version Product Version",
Version: "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
},
{
name: "Higher semantic version File Version",
versionResources: map[string]string{
"FileDescription": "Higher semantic version File Version",
"FileVersion": "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
"ProductVersion": "3.0.0",
},
expectedPackage: pkg.Package{
Name: "Higher semantic version File Version",
Version: "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
},
{
name: "Invalid semantic version File Version",
versionResources: map[string]string{
"FileDescription": "Invalid semantic version File Version",
"FileVersion": "A",
"ProductVersion": "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
expectedPackage: pkg.Package{
Name: "Invalid semantic version File Version",
Version: "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
},
{
name: "Invalid semantic version File Version",
versionResources: map[string]string{
"FileDescription": "Invalid semantic version File Version",
"FileVersion": "A",
"ProductVersion": "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
expectedPackage: pkg.Package{
Name: "Invalid semantic version File Version",
Version: "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
},
{
name: "Invalid semantic version Product Version",
versionResources: map[string]string{
"FileDescription": "Invalid semantic version Product Version",
"FileVersion": "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
"ProductVersion": "A",
},
expectedPackage: pkg.Package{
Name: "Invalid semantic version Product Version",
Version: "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
},
{
name: "Semantically equal falls through, chooses File Version with more components",
versionResources: map[string]string{
"FileDescription": "Semantically equal falls through, chooses File Version with more components",
"FileVersion": "3.0.0.0",
"ProductVersion": "3.0.0",
},
expectedPackage: pkg.Package{
Name: "Semantically equal falls through, chooses File Version with more components",
Version: "3.0.0.0",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
location := file.NewLocation("")
got := newDotnetBinaryPackage(tc.versionResources, location)
// ignore certain metadata
if tc.expectedPackage.Metadata == nil {
got.Metadata = nil
}
// set known defaults
if tc.expectedPackage.Type == "" {
tc.expectedPackage.Type = pkg.DotnetPkg
}
if tc.expectedPackage.Language == "" {
tc.expectedPackage.Language = pkg.Dotnet
}
if tc.expectedPackage.PURL == "" {
tc.expectedPackage.PURL = binaryPackageURL(tc.expectedPackage.Name, tc.expectedPackage.Version)
}
tc.expectedPackage.Locations = file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
pkgtest.AssertPackagesEqual(t, tc.expectedPackage, got)
})
}
}

View File

@ -1,131 +0,0 @@
package dotnet
import (
"context"
"encoding/json"
"fmt"
"sort"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/relationship"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
var _ generic.Parser = parseDotnetDeps
type dotnetDeps struct {
RuntimeTarget dotnetRuntimeTarget `json:"runtimeTarget"`
Targets map[string]map[string]dotnetDepsTarget `json:"targets"`
Libraries map[string]dotnetDepsLibrary `json:"libraries"`
}
type dotnetRuntimeTarget struct {
Name string `json:"name"`
}
type dotnetDepsTarget struct {
Dependencies map[string]string `json:"dependencies"`
Runtime map[string]struct{} `json:"runtime"`
}
type dotnetDepsLibrary struct {
Type string `json:"type"`
Path string `json:"path"`
Sha512 string `json:"sha512"`
HashPath string `json:"hashPath"`
}
//nolint:funlen
func parseDotnetDeps(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
var pkgMap = make(map[string]pkg.Package)
var relationships []artifact.Relationship
dec := json.NewDecoder(reader)
var depsDoc dotnetDeps
if err := dec.Decode(&depsDoc); err != nil {
return nil, nil, fmt.Errorf("failed to parse deps.json file: %w", err)
}
rootName := getDepsJSONFilePrefix(reader.Path())
if rootName == "" {
return nil, nil, fmt.Errorf("unable to determine root package name from deps.json file: %s", reader.Path())
}
var rootPkg *pkg.Package
for nameVersion, lib := range depsDoc.Libraries {
name, _ := extractNameAndVersion(nameVersion)
if lib.Type == "project" && name == rootName {
rootPkg = newDotnetDepsPackage(
nameVersion,
lib,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
)
}
}
if rootPkg == nil {
return nil, nil, fmt.Errorf("unable to determine root package from deps.json file: %s", reader.Path())
}
pkgs = append(pkgs, *rootPkg)
pkgMap[createNameAndVersion(rootPkg.Name, rootPkg.Version)] = *rootPkg
var names []string
for nameVersion := range depsDoc.Libraries {
names = append(names, nameVersion)
}
// sort the names so that the order of the packages is deterministic
sort.Strings(names)
for _, nameVersion := range names {
// skip the root package
name, version := extractNameAndVersion(nameVersion)
if name == rootPkg.Name && version == rootPkg.Version {
continue
}
lib := depsDoc.Libraries[nameVersion]
dotnetPkg := newDotnetDepsPackage(
nameVersion,
lib,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
)
if dotnetPkg != nil {
pkgs = append(pkgs, *dotnetPkg)
pkgMap[nameVersion] = *dotnetPkg
}
}
for pkgNameVersion, target := range depsDoc.Targets[depsDoc.RuntimeTarget.Name] {
for depName, depVersion := range target.Dependencies {
depNameVersion := createNameAndVersion(depName, depVersion)
depPkg, ok := pkgMap[depNameVersion]
if !ok {
log.Debug("unable to find package in map", depNameVersion)
continue
}
p, ok := pkgMap[pkgNameVersion]
if !ok {
log.Debug("unable to find package in map", pkgNameVersion)
continue
}
rel := artifact.Relationship{
From: depPkg,
To: p,
Type: artifact.DependencyOfRelationship,
}
relationships = append(relationships, rel)
}
}
// sort the relationships for deterministic output
// TODO: ideally this would be replaced with artifact.SortRelationships when one exists and is type agnostic.
// this will only consider package-to-package relationships.
relationship.Sort(relationships)
return pkgs, relationships, unknown.IfEmptyf(pkgs, "unable to determine packages")
}

View File

@ -1,366 +0,0 @@
package dotnet
import (
"testing"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func Test_corruptDotnetDeps(t *testing.T) {
pkgtest.NewCatalogTester().
FromFile(t, "test-fixtures/glob-paths/src/something.deps.json").
WithError().
TestParser(t, parseDotnetDeps)
}
func TestParseDotnetDeps(t *testing.T) {
fixture := "test-fixtures/TestLibrary.deps.json"
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture))
rootPkg := pkg.Package{
Name: "TestLibrary",
Version: "1.0.0",
PURL: "pkg:nuget/TestLibrary@1.0.0",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetDepsEntry{
Name: "TestLibrary",
Version: "1.0.0",
},
}
testCommon := pkg.Package{
Name: "TestCommon",
Version: "1.0.0",
PURL: "pkg:nuget/TestCommon@1.0.0",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetDepsEntry{
Name: "TestCommon",
Version: "1.0.0",
},
}
awssdkcore := pkg.Package{
Name: "AWSSDK.Core",
Version: "3.7.10.6",
PURL: "pkg:nuget/AWSSDK.Core@3.7.10.6",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetDepsEntry{
Name: "AWSSDK.Core",
Version: "3.7.10.6",
Sha512: "sha512-kHBB+QmosVaG6DpngXQ8OlLVVNMzltNITfsRr68Z90qO7dSqJ2EHNd8dtBU1u3AQQLqqFHOY0lfmbpexeH6Pew==",
Path: "awssdk.core/3.7.10.6",
HashPath: "awssdk.core.3.7.10.6.nupkg.sha512",
},
}
msftDependencyInjectionAbstractions := pkg.Package{
Name: "Microsoft.Extensions.DependencyInjection.Abstractions",
Version: "6.0.0",
PURL: "pkg:nuget/Microsoft.Extensions.DependencyInjection.Abstractions@6.0.0",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetDepsEntry{
Name: "Microsoft.Extensions.DependencyInjection.Abstractions",
Version: "6.0.0",
Sha512: "sha512-xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==",
Path: "microsoft.extensions.dependencyinjection.abstractions/6.0.0",
HashPath: "microsoft.extensions.dependencyinjection.abstractions.6.0.0.nupkg.sha512",
},
}
msftDependencyInjection := pkg.Package{
Name: "Microsoft.Extensions.DependencyInjection",
Version: "6.0.0",
PURL: "pkg:nuget/Microsoft.Extensions.DependencyInjection@6.0.0",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetDepsEntry{
Name: "Microsoft.Extensions.DependencyInjection",
Version: "6.0.0",
Sha512: "sha512-k6PWQMuoBDGGHOQTtyois2u4AwyVcIwL2LaSLlTZQm2CYcJ1pxbt6jfAnpWmzENA/wfrYRI/X9DTLoUkE4AsLw==",
Path: "microsoft.extensions.dependencyinjection/6.0.0",
HashPath: "microsoft.extensions.dependencyinjection.6.0.0.nupkg.sha512",
},
}
msftLoggingAbstractions := pkg.Package{
Name: "Microsoft.Extensions.Logging.Abstractions",
Version: "6.0.0",
PURL: "pkg:nuget/Microsoft.Extensions.Logging.Abstractions@6.0.0",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetDepsEntry{
Name: "Microsoft.Extensions.Logging.Abstractions",
Version: "6.0.0",
Sha512: "sha512-/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==",
Path: "microsoft.extensions.logging.abstractions/6.0.0",
HashPath: "microsoft.extensions.logging.abstractions.6.0.0.nupkg.sha512",
},
}
msftExtensionsLogging := pkg.Package{
Name: "Microsoft.Extensions.Logging",
Version: "6.0.0",
PURL: "pkg:nuget/Microsoft.Extensions.Logging@6.0.0",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetDepsEntry{
Name: "Microsoft.Extensions.Logging",
Version: "6.0.0",
Sha512: "sha512-eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==",
Path: "microsoft.extensions.logging/6.0.0",
HashPath: "microsoft.extensions.logging.6.0.0.nupkg.sha512",
},
}
msftExtensionsOptions := pkg.Package{
Name: "Microsoft.Extensions.Options",
Version: "6.0.0",
PURL: "pkg:nuget/Microsoft.Extensions.Options@6.0.0",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetDepsEntry{
Name: "Microsoft.Extensions.Options",
Version: "6.0.0",
Sha512: "sha512-dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==",
Path: "microsoft.extensions.options/6.0.0",
HashPath: "microsoft.extensions.options.6.0.0.nupkg.sha512",
},
}
msftExtensionsPrimitives := pkg.Package{
Name: "Microsoft.Extensions.Primitives",
Version: "6.0.0",
PURL: "pkg:nuget/Microsoft.Extensions.Primitives@6.0.0",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetDepsEntry{
Name: "Microsoft.Extensions.Primitives",
Version: "6.0.0",
Sha512: "sha512-9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==",
Path: "microsoft.extensions.primitives/6.0.0",
HashPath: "microsoft.extensions.primitives.6.0.0.nupkg.sha512",
},
}
newtonsoftJson := pkg.Package{
Name: "Newtonsoft.Json",
Version: "13.0.1",
PURL: "pkg:nuget/Newtonsoft.Json@13.0.1",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetDepsEntry{
Name: "Newtonsoft.Json",
Version: "13.0.1",
Sha512: "sha512-ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==",
Path: "newtonsoft.json/13.0.1",
HashPath: "newtonsoft.json.13.0.1.nupkg.sha512",
},
}
serilogSinksConsole := pkg.Package{
Name: "Serilog.Sinks.Console",
Version: "4.0.1",
PURL: "pkg:nuget/Serilog.Sinks.Console@4.0.1",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetDepsEntry{
Name: "Serilog.Sinks.Console",
Version: "4.0.1",
Sha512: "sha512-apLOvSJQLlIbKlbx+Y2UDHSP05kJsV7mou+fvJoRGs/iR+jC22r8cuFVMjjfVxz/AD4B2UCltFhE1naRLXwKNw==",
Path: "serilog.sinks.console/4.0.1",
HashPath: "serilog.sinks.console.4.0.1.nupkg.sha512",
},
}
serilog := pkg.Package{
Name: "Serilog",
Version: "2.10.0",
PURL: "pkg:nuget/Serilog@2.10.0",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetDepsEntry{
Name: "Serilog",
Version: "2.10.0",
Sha512: "sha512-+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA==",
Path: "serilog/2.10.0",
HashPath: "serilog.2.10.0.nupkg.sha512",
},
}
systemDiagnosticsDiagnosticsource := pkg.Package{
Name: "System.Diagnostics.DiagnosticSource",
Version: "6.0.0",
PURL: "pkg:nuget/System.Diagnostics.DiagnosticSource@6.0.0",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetDepsEntry{
Name: "System.Diagnostics.DiagnosticSource",
Version: "6.0.0",
Sha512: "sha512-frQDfv0rl209cKm1lnwTgFPzNigy2EKk1BS3uAvHvlBVKe5cymGyHO+Sj+NLv5VF/AhHsqPIUUwya5oV4CHMUw==",
Path: "system.diagnostics.diagnosticsource/6.0.0",
HashPath: "system.diagnostics.diagnosticsource.6.0.0.nupkg.sha512",
},
}
systemRuntimeCompilerServicesUnsafe := pkg.Package{
Name: "System.Runtime.CompilerServices.Unsafe",
Version: "6.0.0",
PURL: "pkg:nuget/System.Runtime.CompilerServices.Unsafe@6.0.0",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetDepsEntry{
Name: "System.Runtime.CompilerServices.Unsafe",
Version: "6.0.0",
Sha512: "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
Path: "system.runtime.compilerservices.unsafe/6.0.0",
HashPath: "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512",
}}
expectedPkgs := []pkg.Package{
awssdkcore,
msftDependencyInjection,
msftDependencyInjectionAbstractions,
msftExtensionsLogging,
msftLoggingAbstractions,
msftExtensionsOptions,
msftExtensionsPrimitives,
newtonsoftJson,
serilog,
serilogSinksConsole,
systemDiagnosticsDiagnosticsource,
systemRuntimeCompilerServicesUnsafe,
testCommon,
rootPkg,
}
// ┌── (✓ = is represented in the test)
// ↓
//
// ✓ TestLibrary/1.0.0 (project)
// ✓ ├── [a] Microsoft.Extensions.DependencyInjection/6.0.0 [file version: 6.0.21.52210]
// ✓ │ ├── [b] Microsoft.Extensions.DependencyInjection.Abstractions/6.0.0 [file version: 6.0.21.52210]
// ✓ │ └── [c!] System.Runtime.CompilerServices.Unsafe/6.0.0 [NO TARGET INFO]
// ✓ ├── Microsoft.Extensions.Logging/6.0.0 [file version: 6.0.21.52210]
// ✓ │ ├── Microsoft.Extensions.DependencyInjection/6.0.0 ...to [a]
// ✓ │ ├── Microsoft.Extensions.DependencyInjection.Abstractions/6.0.0 ...to [b]
// ✓ │ ├── Microsoft.Extensions.Logging.Abstractions/6.0.0 [file version: 6.0.21.52210]
// ✓ │ ├── Microsoft.Extensions.Options/6.0.0 [file version: 6.0.21.52210]
// ✓ │ │ ├── Microsoft.Extensions.DependencyInjection.Abstractions/6.0.0 ...to [b]
// ✓ │ │ └── Microsoft.Extensions.Primitives/6.0.0 [file version: 6.0.21.52210]
// ✓ │ │ └── System.Runtime.CompilerServices.Unsafe/6.0.0 ...to [c!]
// ✓ │ └── System.Diagnostics.DiagnosticSource/6.0.0 [NO RUNTIME INFO]
// ✓ │ └── System.Runtime.CompilerServices.Unsafe/6.0.0 ...to [c!]
// ✓ ├── Newtonsoft.Json/13.0.1 [file version: 13.0.1.25517]
// ✓ ├── [d] Serilog/2.10.0 [file version: 2.10.0.0]
// ✓ ├── Serilog.Sinks.Console/4.0.1 [file version: 4.0.1.0]
// ✓ │ └── Serilog/2.10.0 ...to [d]
// ✓ └── [e!] TestCommon/1.0.0 [NOT SERVICEABLE / NO SHA]
// ✓ └── AWSSDK.Core/3.7.10.6 [file version: 3.7.10.6]
expectedRelationships := []artifact.Relationship{
{
From: awssdkcore,
To: testCommon,
Type: artifact.DependencyOfRelationship,
},
{
From: msftDependencyInjection,
To: msftExtensionsLogging,
Type: artifact.DependencyOfRelationship,
},
{
From: msftDependencyInjection,
To: rootPkg,
Type: artifact.DependencyOfRelationship,
},
{
From: msftDependencyInjectionAbstractions,
To: msftDependencyInjection,
Type: artifact.DependencyOfRelationship,
},
{
From: msftDependencyInjectionAbstractions,
To: msftExtensionsLogging,
Type: artifact.DependencyOfRelationship,
},
{
From: msftDependencyInjectionAbstractions,
To: msftExtensionsOptions,
Type: artifact.DependencyOfRelationship,
},
{
From: msftExtensionsLogging,
To: rootPkg,
Type: artifact.DependencyOfRelationship,
},
{
From: msftLoggingAbstractions,
To: msftExtensionsLogging,
Type: artifact.DependencyOfRelationship,
},
{
From: msftExtensionsOptions,
To: msftExtensionsLogging,
Type: artifact.DependencyOfRelationship,
},
{
From: msftExtensionsPrimitives,
To: msftExtensionsOptions,
Type: artifact.DependencyOfRelationship,
},
{
From: newtonsoftJson,
To: rootPkg,
Type: artifact.DependencyOfRelationship,
},
{
From: serilog,
To: serilogSinksConsole,
Type: artifact.DependencyOfRelationship,
},
{
From: serilog,
To: rootPkg,
Type: artifact.DependencyOfRelationship,
},
{
From: serilogSinksConsole,
To: rootPkg,
Type: artifact.DependencyOfRelationship,
},
{
From: systemDiagnosticsDiagnosticsource,
To: msftExtensionsLogging,
Type: artifact.DependencyOfRelationship,
},
{
From: systemRuntimeCompilerServicesUnsafe,
To: msftDependencyInjection,
Type: artifact.DependencyOfRelationship,
},
{
From: systemRuntimeCompilerServicesUnsafe,
To: msftExtensionsPrimitives,
Type: artifact.DependencyOfRelationship,
},
{
From: systemRuntimeCompilerServicesUnsafe,
To: systemDiagnosticsDiagnosticsource,
Type: artifact.DependencyOfRelationship,
},
{
From: testCommon,
To: rootPkg,
Type: artifact.DependencyOfRelationship,
},
}
pkgtest.TestFileParser(t, fixture, parseDotnetDeps, expectedPkgs, expectedRelationships)
}

View File

@ -1,248 +0,0 @@
package dotnet
import (
"context"
"fmt"
"io"
"regexp"
"strings"
"github.com/saferwall/pe"
version "github.com/anchore/go-version"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
type dotnetPortableExecutableParser struct {
cfg CatalogerConfig
}
func (p dotnetPortableExecutableParser) parseDotnetPortableExecutable(_ context.Context, _ file.Resolver, _ *generic.Environment, f file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
by, err := io.ReadAll(f)
if err != nil {
return nil, nil, fmt.Errorf("unable to read file: %w", err)
}
peFile, err := pe.NewBytes(by, &pe.Options{DisableCertValidation: !p.cfg.EnableCertificateValidation})
if err != nil {
log.Tracef("unable to create PE instance for file '%s': %v", f.RealPath, err)
return nil, nil, err
}
err = peFile.Parse()
if err != nil {
log.Tracef("unable to parse PE file '%s': %v", f.RealPath, err)
return nil, nil, err
}
versionResources, err := peFile.ParseVersionResources()
if err != nil {
log.Tracef("unable to parse version resources in PE file: %s: %v", f.RealPath, err)
return nil, nil, fmt.Errorf("unable to parse version resources in PE file: %w", err)
}
dotNetPkg, err := buildDotNetPackage(versionResources, f)
if err != nil {
log.Tracef("unable to build dotnet package for: %v %v", f.RealPath, err)
return nil, nil, err
}
return []pkg.Package{dotNetPkg}, nil, nil
}
func buildDotNetPackage(versionResources map[string]string, f file.LocationReadCloser) (dnpkg pkg.Package, err error) {
name := findName(versionResources)
if name == "" {
return dnpkg, fmt.Errorf("unable to find PE name in file")
}
version := findVersion(versionResources)
if version == "" {
return dnpkg, fmt.Errorf("unable to find PE version in file")
}
metadata := pkg.DotnetPortableExecutableEntry{
AssemblyVersion: versionResources["Assembly Version"],
LegalCopyright: versionResources["LegalCopyright"],
Comments: versionResources["Comments"],
InternalName: versionResources["InternalName"],
CompanyName: versionResources["CompanyName"],
ProductName: versionResources["ProductName"],
ProductVersion: versionResources["ProductVersion"],
}
dnpkg = pkg.Package{
Name: name,
Version: version,
Locations: file.NewLocationSet(f.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
Type: pkg.DotnetPkg,
Language: pkg.Dotnet,
PURL: portableExecutablePackageURL(name, version),
Metadata: metadata,
}
dnpkg.SetID()
return dnpkg, nil
}
func portableExecutablePackageURL(name, version string) string {
return packageurl.NewPackageURL(
packageurl.TypeNuget, // See explanation in syft/pkg/cataloger/dotnet/package.go as to why this was chosen.
"",
name,
version,
nil,
"",
).ToString()
}
func extractVersion(version string) string {
version = strings.TrimSpace(version)
out := ""
// some example versions are: "1, 0, 0, 0", "Release 73" or "4.7.4076.0 built by: NET472REL1LAST_B"
// so try to split it and take the first parts that look numeric
for i, f := range strings.Fields(version) {
// if the output already has a number but the current segment does not have a number,
// return what we found for the version
if containsNumber(out) && !containsNumber(f) {
return out
}
if i == 0 {
out = f
} else {
out += " " + f
}
}
return out
}
func keepGreaterSemanticVersion(productVersion string, fileVersion string) string {
semanticProductVersion, err := version.NewVersion(productVersion)
if err != nil || semanticProductVersion == nil {
log.Tracef("Unable to create semantic version from portable executable product version %s", productVersion)
return ""
}
semanticFileVersion, err := version.NewVersion(fileVersion)
if err != nil || semanticFileVersion == nil {
log.Tracef("Unable to create semantic version from portable executable file version %s", fileVersion)
return productVersion
}
// Make no choice when they are semantically equal so that it falls
// through to the other comparison cases
if semanticProductVersion.Equal(semanticFileVersion) {
return ""
}
if semanticFileVersion.GreaterThan(semanticProductVersion) {
return fileVersion
}
return productVersion
}
func findVersion(versionResources map[string]string) string {
productVersion := extractVersion(versionResources["ProductVersion"])
fileVersion := extractVersion(versionResources["FileVersion"])
semanticVersionCompareResult := keepGreaterSemanticVersion(productVersion, fileVersion)
if semanticVersionCompareResult != "" {
return semanticVersionCompareResult
}
productVersionDetail := punctuationCount(productVersion)
fileVersionDetail := punctuationCount(fileVersion)
if containsNumber(productVersion) && productVersionDetail >= fileVersionDetail {
return productVersion
}
if containsNumber(fileVersion) && fileVersionDetail > 0 {
return fileVersion
}
if containsNumber(productVersion) {
return productVersion
}
if containsNumber(fileVersion) {
return fileVersion
}
return productVersion
}
func containsNumber(s string) bool {
return numberRegex.MatchString(s)
}
func punctuationCount(s string) int {
return len(versionPunctuationRegex.FindAllString(s, -1))
}
var (
// spaceRegex includes nbsp (#160) considered to be a space character
spaceRegex = regexp.MustCompile(`[\s\xa0]+`)
numberRegex = regexp.MustCompile(`\d`)
versionPunctuationRegex = regexp.MustCompile(`[.,]+`)
)
func findName(versionResources map[string]string) string {
// PE files found in the wild _not_ authored by Microsoft seem to use ProductName as a clear
// identifier of the software
nameFields := []string{"ProductName", "FileDescription", "InternalName", "OriginalFilename"}
if isMicrosoft(versionResources) {
// Microsoft seems to be consistent using the FileDescription, with a few that are blank and have
// fallbacks to ProductName last, as this is often something very broad like "Microsoft Windows"
nameFields = []string{"FileDescription", "InternalName", "OriginalFilename", "ProductName"}
}
for _, field := range nameFields {
value := spaceNormalize(versionResources[field])
if value == "" {
continue
}
return value
}
return ""
}
// normalizes a string to a trimmed version with all contigous whitespace collapsed to a single space character
func spaceNormalize(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
// ensure valid utf8 text
value = strings.ToValidUTF8(value, "")
// consolidate all space characters
value = spaceRegex.ReplaceAllString(value, " ")
// remove other non-space, non-printable characters
value = regexp.MustCompile(`[\x00-\x1f]`).ReplaceAllString(value, "")
// consolidate all space characters again in case other non-printables were in-between
value = spaceRegex.ReplaceAllString(value, " ")
// finally, remove any remaining surrounding whitespace
value = strings.TrimSpace(value)
return value
}
func isMicrosoft(versionResources map[string]string) bool {
return strings.Contains(strings.ToLower(versionResources["CompanyName"]), "microsoft") ||
strings.Contains(strings.ToLower(versionResources["ProductName"]), "microsoft")
}

View File

@ -1,358 +0,0 @@
package dotnet
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func TestParseDotnetPortableExecutable(t *testing.T) {
tests := []struct {
name string
versionResources map[string]string
expectedPackage pkg.Package
}{
{
name: "dotnet package with extra version info",
versionResources: map[string]string{
"InternalName": "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll",
"FileVersion": "3.14.40721.0918 xxxfffdddjjjj",
"FileDescription": "Active Directory Authentication Library",
"ProductName": "Active Directory Authentication Library",
"Comments": "",
"CompanyName": "Microsoft Corporation",
"LegalTrademarks": "",
"LegalCopyright": "Copyright (c) Microsoft Corporation. All rights reserved.",
"OriginalFilename": "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll",
"ProductVersion": "c61f043686a544863efc014114c42e844f905336",
"Assembly Version": "3.14.2.11",
},
expectedPackage: pkg.Package{
Name: "Active Directory Authentication Library",
Version: "3.14.40721.0918",
Metadata: pkg.DotnetPortableExecutableEntry{
AssemblyVersion: "3.14.2.11",
LegalCopyright: "Copyright (c) Microsoft Corporation. All rights reserved.",
InternalName: "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll",
CompanyName: "Microsoft Corporation",
ProductName: "Active Directory Authentication Library",
ProductVersion: "c61f043686a544863efc014114c42e844f905336",
},
},
},
{
name: "dotnet package with malformed field and extended version",
versionResources: map[string]string{
"CompanyName": "Microsoft Corporation",
"FileDescription": "äbFile\xa0\xa1Versi on",
"FileVersion": "4.6.25512.01 built by: dlab-DDVSOWINAGE016. Commit Hash: d0d5c7b49271cadb6d97de26d8e623e98abdc8db",
"InternalName": "äbFileVersion",
"LegalCopyright": "© Microsoft Corporation. All rights reserved.",
"OriginalFilename": "TProductName",
"ProductName": "Microsoft® .NET Framework",
"ProductVersion": "4.6.25512.01 built by: dlab-DDVSOWINAGE016. Commit Hash: d0d5c7b49271cadb6d97de26d8e623e98abdc8db",
},
expectedPackage: pkg.Package{
Name: "äbFileVersi on",
Version: "4.6.25512.01",
PURL: "pkg:nuget/%C3%A4bFileVersi%20on@4.6.25512.01",
Metadata: pkg.DotnetPortableExecutableEntry{
LegalCopyright: "© Microsoft Corporation. All rights reserved.",
InternalName: "äb\x01FileVersion",
CompanyName: "Microsoft Corporation",
ProductName: "Microsoft® .NET Framework",
ProductVersion: "4.6.25512.01 built by: dlab-DDVSOWINAGE016. Commit Hash: d0d5c7b49271cadb6d97de26d8e623e98abdc8db",
},
},
},
{
name: "System.Data.Linq.dll",
versionResources: map[string]string{
"CompanyName": "Microsoft Corporation",
"FileDescription": "System.Data.Linq.dll",
"FileVersion": "4.7.3190.0 built by: NET472REL1LAST_C",
"InternalName": "System.Data.Linq.dll",
"LegalCopyright": "© Microsoft Corporation. All rights reserved.",
"OriginalFilename": "System.Data.Linq.dll",
"ProductName": "Microsoft® .NET Framework",
"ProductVersion": "4.7.3190.0",
},
expectedPackage: pkg.Package{
Name: "System.Data.Linq.dll",
Version: "4.7.3190.0",
},
},
{
name: "curl",
versionResources: map[string]string{
"CompanyName": "curl, https://curl.se/",
"FileDescription": "The curl executable",
"FileVersion": "8.4.0",
"InternalName": "curl",
"LegalCopyright": "© Daniel Stenberg, <daniel@haxx.se>.",
"OriginalFilename": "curl.exe",
"ProductName": "The curl executable",
"ProductVersion": "8.4.0",
},
expectedPackage: pkg.Package{
Name: "The curl executable",
Version: "8.4.0",
},
},
{
name: "Prometheus",
versionResources: map[string]string{
"AssemblyVersion": "8.0.0.0",
"CompanyName": "",
"FileDescription": "",
"FileVersion": "8.0.1",
"InternalName": "Prometheus.AspNetCore.dll",
"OriginalFilename": "Prometheus.AspNetCore.dll",
"ProductName": "",
"ProductVersion": "8.0.1",
},
expectedPackage: pkg.Package{
Name: "Prometheus.AspNetCore.dll",
Version: "8.0.1",
},
},
{
name: "Hidden Input",
versionResources: map[string]string{
"FileDescription": "Reads from stdin without leaking info to the terminal and outputs back to stdout",
"FileVersion": "1, 0, 0, 0",
"InternalName": "hiddeninput",
"LegalCopyright": "Jordi Boggiano - 2012",
"OriginalFilename": "hiddeninput.exe",
"ProductName": "Hidden Input",
"ProductVersion": "1, 0, 0, 0",
},
expectedPackage: pkg.Package{
Name: "Hidden Input",
Version: "1, 0, 0, 0",
},
},
{
name: "SQLite3",
versionResources: map[string]string{
"CompanyName": "SQLite Development Team",
"FileDescription": "SQLite is a software library that implements a self-contained, serverless, zero-configuration, transactional SQL database engine.",
"FileVersion": "3.23.2",
"InternalName": "sqlite3",
"LegalCopyright": "http://www.sqlite.org/copyright.html",
"ProductName": "SQLite",
"ProductVersion": "3.23.2",
},
expectedPackage: pkg.Package{
Name: "SQLite",
Version: "3.23.2",
},
},
{
name: "Brave Browser",
versionResources: map[string]string{
"CompanyName": "Brave Software, Inc.",
"FileDescription": "Brave Browser",
"FileVersion": "80.1.7.92",
"InternalName": "chrome_exe",
"LegalCopyright": "Copyright 2016 The Brave Authors. All rights reserved.",
"OriginalFilename": "chrome.exe",
"ProductName": "Brave Browser",
"ProductVersion": "80.1.7.92",
},
expectedPackage: pkg.Package{
Name: "Brave Browser",
Version: "80.1.7.92",
},
},
{
name: "Better product version",
versionResources: map[string]string{
"FileDescription": "Better version",
"FileVersion": "80.1.7",
"ProductVersion": "80.1.7.92",
},
expectedPackage: pkg.Package{
Name: "Better version",
Version: "80.1.7.92",
},
},
{
name: "Better file version",
versionResources: map[string]string{
"FileDescription": "Better version",
"FileVersion": "80.1.7.92",
"ProductVersion": "80.1.7",
},
expectedPackage: pkg.Package{
Name: "Better version",
Version: "80.1.7.92",
},
},
{
name: "Higher semantic version Product Version",
versionResources: map[string]string{
"FileDescription": "Higher semantic version Product Version",
"FileVersion": "3.0.0.0",
"ProductVersion": "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
expectedPackage: pkg.Package{
Name: "Higher semantic version Product Version",
Version: "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
},
{
name: "Higher semantic version File Version",
versionResources: map[string]string{
"FileDescription": "Higher semantic version File Version",
"FileVersion": "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
"ProductVersion": "3.0.0",
},
expectedPackage: pkg.Package{
Name: "Higher semantic version File Version",
Version: "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
},
{
name: "Invalid semantic version File Version",
versionResources: map[string]string{
"FileDescription": "Invalid semantic version File Version",
"FileVersion": "A",
"ProductVersion": "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
expectedPackage: pkg.Package{
Name: "Invalid semantic version File Version",
Version: "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
},
{
name: "Invalid semantic version File Version",
versionResources: map[string]string{
"FileDescription": "Invalid semantic version File Version",
"FileVersion": "A",
"ProductVersion": "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
expectedPackage: pkg.Package{
Name: "Invalid semantic version File Version",
Version: "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
},
{
name: "Invalid semantic version Product Version",
versionResources: map[string]string{
"FileDescription": "Invalid semantic version Product Version",
"FileVersion": "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
"ProductVersion": "A",
},
expectedPackage: pkg.Package{
Name: "Invalid semantic version Product Version",
Version: "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
},
{
name: "Semantically equal falls through, chooses File Version with more components",
versionResources: map[string]string{
"FileDescription": "Semantically equal falls through, chooses File Version with more components",
"FileVersion": "3.0.0.0",
"ProductVersion": "3.0.0",
},
expectedPackage: pkg.Package{
Name: "Semantically equal falls through, chooses File Version with more components",
Version: "3.0.0.0",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
location := file.NewLocation("")
f := file.LocationReadCloser{
Location: location,
}
got, err := buildDotNetPackage(tc.versionResources, f)
assert.NoErrorf(t, err, "failed to build package from version resources: %+v", tc.versionResources)
// ignore certain metadata
if tc.expectedPackage.Metadata == nil {
got.Metadata = nil
}
// set known defaults
if tc.expectedPackage.Type == "" {
tc.expectedPackage.Type = pkg.DotnetPkg
}
if tc.expectedPackage.Language == "" {
tc.expectedPackage.Language = pkg.Dotnet
}
if tc.expectedPackage.PURL == "" {
tc.expectedPackage.PURL = portableExecutablePackageURL(tc.expectedPackage.Name, tc.expectedPackage.Version)
}
tc.expectedPackage.Locations = file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
pkgtest.AssertPackagesEqual(t, tc.expectedPackage, got)
})
}
}
func Test_corruptDotnetPE(t *testing.T) {
p := dotnetPortableExecutableParser{
cfg: DefaultCatalogerConfig(),
}
pkgtest.NewCatalogTester().
FromFile(t, "test-fixtures/glob-paths/src/something.exe").
WithError().
TestParser(t, p.parseDotnetPortableExecutable)
}
func Test_extractVersion(t *testing.T) {
tests := []struct {
input string
expected string
}{
{
input: "1, 0, 0, 0",
expected: "1, 0, 0, 0",
},
{
input: "Release 73",
expected: "Release 73",
},
{
input: "4.7.4076.0 built by: NET472REL1LAST_B",
expected: "4.7.4076.0",
},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
got := extractVersion(test.input)
assert.Equal(t, test.expected, got)
})
}
}
func Test_spaceNormalize(t *testing.T) {
tests := []struct {
input string
expected string
}{
{
expected: "some spaces apart",
input: " some spaces\n\t\t \n\rapart\n",
},
{
expected: "söme ¡nvalid characters",
input: "\rsöme \u0001¡nvalid\t characters\n",
},
}
for _, test := range tests {
t.Run(test.expected, func(t *testing.T) {
got := spaceNormalize(test.input)
assert.Equal(t, test.expected, got)
})
}
}

View File

@ -13,7 +13,7 @@ func Test_corruptDotnetPackagesLock(t *testing.T) {
pkgtest.NewCatalogTester().
FromFile(t, "test-fixtures/glob-paths/src/packages.lock.json").
WithError().
TestParser(t, parseDotnetDeps)
TestParser(t, parseDotnetPackagesLock)
}
func TestParseDotnetPackagesLock(t *testing.T) {

View File

@ -0,0 +1,740 @@
package dotnet
import (
"bytes"
"debug/pe"
"encoding/binary"
"errors"
"fmt"
"io"
"unicode/utf16"
"github.com/scylladb/go-set/strset"
"github.com/scylladb/go-set/u32set"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
)
const peMaxAllowedDirectoryEntries = 0x1000
var imageDirectoryEntryIndexes = []int{
pe.IMAGE_DIRECTORY_ENTRY_RESOURCE, // where version resources are stored
pe.IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR, // where info about the CLR is stored
}
// logicalPE does not directly represent a binary shape to be parsed, instead it represents the
// information of interest extracted from a PE file.
type logicalPE struct {
// Location is where the PE file was found
Location file.Location
// TargetPath is the path is the deps.json target entry. This is not present in the PE file
// but instead is used in downstream processing to track associations between the PE file and the deps.json file.
TargetPath string
// CLR is the information about the CLR (common language runtime) version found in the PE file which helps
// understand if this executable is even a .NET application.
CLR *clrEvidence
// VersionResources is a map of version resource keys to their values found in the VERSIONINFO resource directory.
VersionResources map[string]string
}
// clrEvidence is basic info about the CLR (common language runtime) version from the COM descriptor.
// This is not a complete representation of the CLR version, but rather a subset of the information that is
// useful to us.
type clrEvidence struct {
// HasClrResourceNames is true if there are CLR resource names found in the PE file (e.g. "CLRDEBUGINFO").
HasClrResourceNames bool
// MajorVersion is the minimum supported major version of the CLR.
MajorVersion uint16
// MinorVersion is the minimum supported minor version of the CLR.
MinorVersion uint16
}
// hasEvidenceOfCLR returns true if the PE file has evidence of a CLR (common language runtime) version.
func (c *clrEvidence) hasEvidenceOfCLR() bool {
return c != nil && (c.MajorVersion != 0 && c.MinorVersion != 0 || c.HasClrResourceNames)
}
type peDosHeader struct {
Magic [2]byte // "MZ"
Unused [58]byte
AddressOfNewEXEHeader uint32 // offset to PE header
}
// peImageCore20 represents the .NET Core 2.0 header structure.
// Source: https://github.com/dotnet/msbuild/blob/9fa9d800dabce3bfcf8365f651f3a713e01f8a85/src/Tasks/NativeMethods.cs#L761-L775
type peImageCore20 struct {
Cb uint32
MajorRuntimeVersion uint16
MinorRuntimeVersion uint16
}
// peImageResourceDirectory represents the resource directory structure.
type peImageResourceDirectory struct {
Characteristics uint32
TimeDateStamp uint32
MajorVersion uint16
MinorVersion uint16
NumberOfNamedEntries uint16
NumberOfIDEntries uint16
}
// peImageResourceDirectoryEntry represents an entry in the resource directory entries.
type peImageResourceDirectoryEntry struct {
Name uint32
OffsetToData uint32
}
// peImageResourceDataEntry is the unit of raw data in the Resource Data area.
type peImageResourceDataEntry struct {
OffsetToData uint32
Size uint32
CodePage uint32
Reserved uint32
}
// peVsFixedFileInfo represents the fixed file information structure.
type peVsFixedFileInfo struct {
Signature uint32
StructVersion uint32
FileVersionMS uint32
FileVersionLS uint32
ProductVersionMS uint32
ProductVersionLS uint32
FileFlagsMask uint32
FileFlags uint32
FileOS uint32
FileType uint32
FileSubtype uint32
FileDateMS uint32
FileDateLS uint32
}
type peVsVersionInfo peLenValLenType
type peStringFileInfo peLenValLenType
type peStringTable peLenValLenType
type peString peLenValLenType
type peLenValLenType struct {
Length uint16
ValueLength uint16
Type uint16
}
type extractedSection struct {
RVA uint32
BaseRVA uint32
Size uint32
Reader *bytes.Reader
}
func (s extractedSection) exists() bool {
return s.RVA != 0 && s.Size != 0
}
func directoryName(i int) string {
switch i {
case pe.IMAGE_DIRECTORY_ENTRY_RESOURCE:
return "Resource"
case pe.IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR:
return "COM Descriptor"
}
return fmt.Sprintf("Unknown (%d)", i)
}
func getLogicalDotnetPE(f file.LocationReadCloser) (*logicalPE, error) {
r, err := unionreader.GetUnionReader(f)
if err != nil {
return nil, err
}
sections, _, err := parsePEFile(r)
if err != nil {
return nil, fmt.Errorf("unable to parse PE sections: %w", err)
}
dirs := u32set.New() // keep track of the RVAs we have already parsed (prevent infinite recursion edge cases)
versionResources := make(map[string]string) // map of version resource keys to their values
resourceNames := strset.New() // set of resource names found in the PE file
err = parseResourceDirectory(sections[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE], dirs, versionResources, resourceNames)
if err != nil {
return nil, err
}
c, err := parseCLR(sections[pe.IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR], resourceNames)
if err != nil {
return nil, fmt.Errorf("unable to parse PE CLR directory: %w", err)
}
return &logicalPE{
Location: f.Location,
CLR: c,
VersionResources: versionResources,
}, nil
}
// parsePEFile creates readers for targeted sections of the binary used by downstream processing.
func parsePEFile(file unionreader.UnionReader) (map[int]*extractedSection, []pe.SectionHeader32, error) {
fileHeader, magic, err := parsePEHeader(file)
if err != nil {
return nil, nil, fmt.Errorf("error parsing PE header: %w", err)
}
soi, headers, err := parseSectionHeaders(file, magic, fileHeader.NumberOfSections)
if err != nil {
return nil, nil, fmt.Errorf("error parsing section headers: %w", err)
}
for i, sec := range soi {
if !sec.exists() {
continue
}
data, err := readDataFromRVA(file, sec.RVA, sec.Size, headers)
if err != nil {
return nil, nil, fmt.Errorf("error reading %q section data: %w", directoryName(i), err)
}
sec.Reader = data
}
return soi, headers, nil
}
// parsePEHeader reads the beginning of a PE formatted file, returning the file header and "magic" indicator
// for downstream logic to determine 32/64 bit parsing.
func parsePEHeader(file unionreader.UnionReader) (*pe.FileHeader, uint16, error) {
var dosHeader peDosHeader
if err := binary.Read(file, binary.LittleEndian, &dosHeader); err != nil {
return nil, 0, fmt.Errorf("error reading DOS header: %w", err)
}
if string(dosHeader.Magic[:]) != "MZ" {
return nil, 0, fmt.Errorf("invalid DOS header magic")
}
peOffset := int64(dosHeader.AddressOfNewEXEHeader)
if _, err := file.Seek(peOffset, io.SeekStart); err != nil {
return nil, 0, fmt.Errorf("error seeking to PE header: %w", err)
}
var signature [4]byte
if err := binary.Read(file, binary.LittleEndian, &signature); err != nil {
return nil, 0, fmt.Errorf("error reading PE signature: %w", err)
}
if !bytes.Equal(signature[:], []byte("PE\x00\x00")) {
return nil, 0, fmt.Errorf("invalid PE signature")
}
var fileHeader pe.FileHeader
if err := binary.Read(file, binary.LittleEndian, &fileHeader); err != nil {
return nil, 0, fmt.Errorf("error reading file header: %w", err)
}
var magic uint16
if err := binary.Read(file, binary.LittleEndian, &magic); err != nil {
return nil, 0, fmt.Errorf("error reading optional header magic: %w", err)
}
// seek back to before reading magic (since that value is in the header)
if _, err := file.Seek(-2, io.SeekCurrent); err != nil {
return nil, 0, fmt.Errorf("error seeking back to before reading magic: %w", err)
}
return &fileHeader, magic, nil
}
// parseSectionHeaders reads the section headers from the PE file and extracts the virtual addresses + section size
// information for the sections of interest. Additionally, all section headers are returned to aid in downstream processing.
func parseSectionHeaders(file unionreader.UnionReader, magic uint16, numberOfSections uint16) (map[int]*extractedSection, []pe.SectionHeader32, error) {
soi := make(map[int]*extractedSection)
switch magic {
case 0x10B: // PE32
var optHeader pe.OptionalHeader32
if err := binary.Read(file, binary.LittleEndian, &optHeader); err != nil {
return nil, nil, fmt.Errorf("error reading optional header (PE32): %w", err)
}
for _, i := range imageDirectoryEntryIndexes {
sectionHeader := optHeader.DataDirectory[i]
if sectionHeader.Size == 0 {
continue
}
soi[i] = &extractedSection{
RVA: sectionHeader.VirtualAddress,
Size: sectionHeader.Size,
}
}
case 0x20B: // PE32+ (64 bit)
var optHeader pe.OptionalHeader64
if err := binary.Read(file, binary.LittleEndian, &optHeader); err != nil {
return nil, nil, fmt.Errorf("error reading optional header (PE32+): %w", err)
}
for _, i := range imageDirectoryEntryIndexes {
sectionHeader := optHeader.DataDirectory[i]
if sectionHeader.Size == 0 {
continue
}
soi[i] = &extractedSection{
RVA: sectionHeader.VirtualAddress,
Size: sectionHeader.Size,
}
}
default:
return nil, nil, fmt.Errorf("unknown optional header magic: 0x%x", magic)
}
// read section headers
headers := make([]pe.SectionHeader32, numberOfSections)
for i := 0; i < int(numberOfSections); i++ {
if err := binary.Read(file, binary.LittleEndian, &headers[i]); err != nil {
return nil, nil, fmt.Errorf("error reading section header: %w", err)
}
}
return soi, headers, nil
}
// parseCLR extracts the CLR (common language runtime) version information from the COM descriptor and makes
// present/not-present determination based on the presence of CLR resource names.
func parseCLR(sec *extractedSection, resourceNames *strset.Set) (*clrEvidence, error) {
hasCLRDebugResourceNames := resourceNames.HasAny("CLRDEBUGINFO")
if sec == nil || sec.Reader == nil {
return &clrEvidence{
HasClrResourceNames: hasCLRDebugResourceNames,
}, nil
}
reader := sec.Reader
var c peImageCore20
if err := binary.Read(reader, binary.LittleEndian, &c); err != nil {
return nil, fmt.Errorf("error reading CLR header: %w", err)
}
return &clrEvidence{
HasClrResourceNames: hasCLRDebugResourceNames,
MajorVersion: c.MajorRuntimeVersion,
MinorVersion: c.MinorRuntimeVersion,
}, nil
}
// rvaToFileOffset is a helper function to convert RVA to file offset using section headers
func rvaToFileOffset(rva uint32, sections []pe.SectionHeader32) (uint32, error) {
for _, section := range sections {
if rva >= section.VirtualAddress && rva < section.VirtualAddress+section.VirtualSize {
return section.PointerToRawData + (rva - section.VirtualAddress), nil
}
}
return 0, fmt.Errorf("RVA 0x%x not found in any section", rva)
}
// readDataFromRVA will read data from a specific RVA in the PE file
func readDataFromRVA(file io.ReadSeeker, rva, size uint32, sections []pe.SectionHeader32) (*bytes.Reader, error) {
if size == 0 {
return nil, fmt.Errorf("zero size specified")
}
offset, err := rvaToFileOffset(rva, sections)
if err != nil {
return nil, err
}
if _, err := file.Seek(int64(offset), io.SeekStart); err != nil {
return nil, fmt.Errorf("error seeking to data: %w", err)
}
data := make([]byte, size)
if _, err := io.ReadFull(file, data); err != nil {
return nil, fmt.Errorf("error reading data: %w", err)
}
return bytes.NewReader(data), nil
}
// parseResourceDirectory recursively parses a PE resource directory. This takes a relative virtual address (offset of
// a piece of data or code relative to the base address), the size of the resource directory, the set of RVAs already
// parsed, and the map to populate discovered version resource values.
//
// .rsrc Section
// +------------------------------+
// | Resource Directory Table |
// +------------------------------+
// | Resource Directory Entries |
// | +------------------------+ |
// | | Subdirectory or Data | |
// | +------------------------+ |
// +------------------------------+
// | Resource Data Entries |
// | +------------------------+ |
// | | Resource Data | |
// | +------------------------+ |
// +------------------------------+
// | Actual Resource Data |
// +------------------------------+
//
// sources:
// - https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-rsrc-section
// - https://learn.microsoft.com/en-us/previous-versions/ms809762(v=msdn.10)#pe-file-resources
func parseResourceDirectory(sec *extractedSection, dirs *u32set.Set, fields map[string]string, names *strset.Set) error {
if sec == nil || sec.Size <= 0 {
return nil
}
if sec.Reader == nil {
return errors.New("resource section not found")
}
baseRVA := sec.BaseRVA
if baseRVA == 0 {
baseRVA = sec.RVA
}
offset := int64(sec.RVA - baseRVA)
if _, err := sec.Reader.Seek(offset, io.SeekStart); err != nil {
return fmt.Errorf("error seeking to directory offset: %w", err)
}
var directoryHeader peImageResourceDirectory
if err := readIntoStruct(sec.Reader, &directoryHeader); err != nil {
return fmt.Errorf("error reading directory header: %w", err)
}
numEntries := int(directoryHeader.NumberOfNamedEntries + directoryHeader.NumberOfIDEntries)
switch {
case numEntries > peMaxAllowedDirectoryEntries:
return fmt.Errorf("too many entries in resource directory: %d", numEntries)
case numEntries == 0:
return fmt.Errorf("no entries in resource directory")
case numEntries < 0:
return fmt.Errorf("invalid number of entries in resource directory: %d", numEntries)
}
for i := 0; i < numEntries; i++ {
var entry peImageResourceDirectoryEntry
entryOffset := offset + int64(binary.Size(directoryHeader)) + int64(i*binary.Size(entry))
if _, err := sec.Reader.Seek(entryOffset, io.SeekStart); err != nil {
log.Tracef("error seeking to PE entry offset: %v", err)
continue
}
if err := readIntoStruct(sec.Reader, &entry); err != nil {
continue
}
if err := processResourceEntry(entry, baseRVA, sec, dirs, fields, names); err != nil {
log.Tracef("error processing resource entry: %v", err)
continue
}
}
return nil
}
func processResourceEntry(entry peImageResourceDirectoryEntry, baseRVA uint32, sec *extractedSection, dirs *u32set.Set, fields map[string]string, names *strset.Set) error {
// if the high bit is set, this is a directory entry, otherwise it is a data entry
isDirectory := entry.OffsetToData&0x80000000 != 0
// note: the offset is relative to the beginning of the resource section, not an RVA
entryOffsetToData := entry.OffsetToData & 0x7FFFFFFF
nameIsString := entry.Name&0x80000000 != 0
nameOffset := entry.Name & 0x7FFFFFFF
// read the string name of the resource directory
if nameIsString {
currentPos, err := sec.Reader.Seek(0, io.SeekCurrent)
if err != nil {
return fmt.Errorf("error getting current reader position: %w", err)
}
if _, err := sec.Reader.Seek(int64(nameOffset), io.SeekStart); err != nil {
return fmt.Errorf("error restoring reader position: %w", err)
}
name, err := readUTF16WithLength(sec.Reader)
if err == nil {
names.Add(name)
}
if _, err := sec.Reader.Seek(currentPos, io.SeekStart); err != nil {
return fmt.Errorf("error restoring reader position: %w", err)
}
}
if isDirectory {
subRVA := baseRVA + entryOffsetToData
if dirs.Has(subRVA) {
// some malware uses recursive PE references to evade analysis
return fmt.Errorf("recursive PE reference detected; skipping directory at baseRVA=0x%x subRVA=0x%x", baseRVA, subRVA)
}
dirs.Add(subRVA)
err := parseResourceDirectory(
&extractedSection{
RVA: subRVA,
BaseRVA: baseRVA,
Size: sec.Size - (sec.RVA - baseRVA),
Reader: sec.Reader,
},
dirs, fields, names)
if err != nil {
return err
}
return nil
}
return parseResourceDataEntry(sec.Reader, baseRVA, baseRVA+entryOffsetToData, sec.Size, fields)
}
func parseResourceDataEntry(reader *bytes.Reader, baseRVA, rva, remainingSize uint32, fields map[string]string) error {
var dataEntry peImageResourceDataEntry
offset := int64(rva - baseRVA)
if _, err := reader.Seek(offset, io.SeekStart); err != nil {
return fmt.Errorf("error seeking to data entry offset: %w", err)
}
if err := readIntoStruct(reader, &dataEntry); err != nil {
return fmt.Errorf("error reading resource data entry: %w", err)
}
if remainingSize < dataEntry.Size {
return fmt.Errorf("resource data entry size exceeds remaining size")
}
data := make([]byte, dataEntry.Size)
if _, err := reader.Seek(int64(dataEntry.OffsetToData-baseRVA), io.SeekStart); err != nil {
return fmt.Errorf("error seeking to resource data: %w", err)
}
if _, err := reader.Read(data); err != nil {
return fmt.Errorf("error reading resource data: %w", err)
}
return parseVersionResourceSection(bytes.NewReader(data), fields)
}
// parseVersionResourceSection parses a PE version resource section from within a resource directory.
//
// "The main structure in a version resource is the VS_FIXEDFILEINFO structure. Additional structures include the
// VarFileInfo structure to store language information data, and StringFileInfo for user-defined string information.
// All strings in a version resource are in Unicode format. Each block of information is aligned on a DWORD boundary."
//
// "VS_VERSIONINFO" (utf16)
// +---------------------------------------------------+
// | wLength (2 bytes) |
// | wValueLength (2 bytes) |
// | wType (2 bytes) |
// | szKey ("VS_VERSION_INFO") (utf16) |
// | Padding (to DWORD) |
// +---------------------------------------------------+
// | VS_FIXEDFILEINFO (52 bytes) |
// +---------------------------------------------------+
// | "StringFileInfo" (utf16) |
// +---------------------------------------------------+
// | wLength (2 bytes) |
// | wValueLength (2 bytes) |
// | wType (2 bytes) |
// | szKey ("StringFileInfo") (utf16) |
// | Padding (to DWORD) |
// | StringTable |
// | +--------------------------------------------+ |
// | | wLength (2 bytes) | |
// | | wValueLength (2 bytes) | |
// | | wType (2 bytes) | |
// | | szKey ("040904b0") | |
// | | Padding (to DWORD) | |
// | | String | |
// | | +--------------------------------------+ | |
// | | | wLength (2 bytes) | | |
// | | | wValueLength (2 bytes) | | |
// | | | wType (2 bytes) | | |
// | | | szKey ("FileVersion") | | |
// | | | Padding (to DWORD) | | |
// | | | szValue ("15.00.0913.015") | | |
// | | | Padding (to DWORD) | | |
// | +--------------------------------------------+ |
// +---------------------------------------------------+
// | VarFileInfo (utf16) |
// +---------------------------------------------------+
// | (skip!) |
// +---------------------------------------------------+
//
// sources:
// - https://learn.microsoft.com/en-us/windows/win32/menurc/resource-file-formats
// - https://learn.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo
// - https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
// - https://learn.microsoft.com/en-us/windows/win32/menurc/varfileinfo
// - https://learn.microsoft.com/en-us/windows/win32/menurc/stringfileinfo
// - https://learn.microsoft.com/en-us/windows/win32/menurc/stringtable
func parseVersionResourceSection(reader *bytes.Reader, fields map[string]string) error {
offset := 0
var info peVsVersionInfo
if szKey, err := readIntoStructAndSzKey(reader, &info, &offset); err != nil {
return fmt.Errorf("error reading PE version info: %v", err)
} else if szKey != "VS_VERSION_INFO" {
// this is a resource section, but not the version resources
return nil
}
if err := alignAndSeek(reader, &offset); err != nil {
return fmt.Errorf("error aligning past PE version info: %w", err)
}
var fixedFileInfo peVsFixedFileInfo
if err := readIntoStruct(reader, &fixedFileInfo, &offset); err != nil {
return fmt.Errorf("error reading PE FixedFileInfo: %v", err)
}
for reader.Len() > 0 {
if err := alignAndSeek(reader, &offset); err != nil {
return fmt.Errorf("error seeking to PE StringFileInfo: %w", err)
}
var sfiHeader peStringFileInfo
if szKey, err := readIntoStructAndSzKey(reader, &sfiHeader, &offset); err != nil {
return fmt.Errorf("error reading PE string file info header: %v", err)
} else if szKey != "StringFileInfo" {
// we only care about extracting strings from any string tables, skip this
offset += int(sfiHeader.ValueLength)
continue
}
var stOffset int
// note: the szKey for the prStringTable is the language
var stHeader peStringTable
if _, err := readIntoStructAndSzKey(reader, &stHeader, &offset, &stOffset); err != nil {
return fmt.Errorf("error reading PE string table header: %v", err)
}
for stOffset < int(stHeader.Length) {
var stringHeader peString
if err := readIntoStruct(reader, &stringHeader, &offset, &stOffset); err != nil {
break
}
key := readUTF16(reader, &offset, &stOffset)
if err := alignAndSeek(reader, &offset, &stOffset); err != nil {
return fmt.Errorf("error aligning to next PE string table value: %w", err)
}
var value string
if stringHeader.ValueLength > 0 {
value = readUTF16(reader, &offset, &stOffset)
}
fields[key] = value
if err := alignAndSeek(reader, &offset, &stOffset); err != nil {
return fmt.Errorf("error aligning to next PE string table key: %w", err)
}
}
}
if fields["FileVersion"] == "" {
// we can derive the file version from the fixed file info if it is not already specified as a string entry... neat!
fields["FileVersion"] = fmt.Sprintf("%d.%d.%d.%d",
fixedFileInfo.FileVersionMS>>16, fixedFileInfo.FileVersionMS&0xFFFF,
fixedFileInfo.FileVersionLS>>16, fixedFileInfo.FileVersionLS&0xFFFF)
}
return nil
}
// readIntoStructAndSzKey reads a struct from the reader and updates the offsets if provided, returning the szKey value.
// This is only useful in the context of the resource directory parsing in narrow cases (this is invalid to use outside of that context).
func readIntoStructAndSzKey[T any](reader *bytes.Reader, data *T, offsets ...*int) (string, error) {
if err := readIntoStruct(reader, data, offsets...); err != nil {
return "", err
}
return readUTF16(reader, offsets...), nil
}
// readIntoStruct reads a struct from the reader and updates the offsets if provided.
func readIntoStruct[T any](reader io.Reader, data *T, offsets ...*int) error {
if err := binary.Read(reader, binary.LittleEndian, data); err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
for i := range offsets {
*offsets[i] += binary.Size(*data)
}
return nil
}
// alignAndSeek aligns the reader to the next DWORD boundary and seeks to the new offset (updating any provided trackOffsets).
func alignAndSeek(reader io.Seeker, offset *int, trackOffsets ...*int) error {
ogOffset := *offset
*offset = alignToDWORD(*offset)
diff := *offset - ogOffset
for i := range trackOffsets {
*trackOffsets[i] += diff
}
_, err := reader.Seek(int64(*offset), io.SeekStart)
return err
}
// alignToDWORD aligns the offset to the next DWORD boundary (4 byte boundary)
func alignToDWORD(offset int) int {
return (offset + 3) & ^3
}
// readUTF16 is a helper function to read a null-terminated UTF16 string
func readUTF16(reader *bytes.Reader, offsets ...*int) string {
startPos, err := reader.Seek(0, io.SeekCurrent)
if err != nil {
return ""
}
var result []rune
for {
var char uint16
err := binary.Read(reader, binary.LittleEndian, &char)
if err != nil || char == 0 {
break
}
result = append(result, rune(char))
}
// calculate how many bytes we've actually read (including null terminator)
endPos, _ := reader.Seek(0, io.SeekCurrent)
bytesRead := int(endPos - startPos)
for i := range offsets {
*offsets[i] += bytesRead
}
return string(result)
}
// readUTF16WithLength reads a length-prefixed UTF-16 string from reader.
// The first 2 bytes represent the number of UTF-16 code units.
func readUTF16WithLength(reader *bytes.Reader) (string, error) {
var length uint16
if err := binary.Read(reader, binary.LittleEndian, &length); err != nil {
return "", err
}
if length == 0 {
return "", nil
}
// read length UTF-16 code units.
codes := make([]uint16, length)
if err := binary.Read(reader, binary.LittleEndian, &codes); err != nil {
return "", err
}
return string(utf16.Decode(codes)), nil
}

View File

@ -0,0 +1,163 @@
package dotnet
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/stereoscopesource"
)
func Test_getLogicalDotnetPE(t *testing.T) {
tests := []struct {
name string
fixture string
path string
wantVR map[string]string
wantCLR bool
wantErr require.ErrorAssertionFunc
}{
{
name: "newtonsoft",
path: "/app/Newtonsoft.Json.dll",
fixture: "image-net8-app",
wantCLR: true,
wantVR: map[string]string{
// the numbers are the field parse order, which helped for debugging and understanding corrupted fields
"Comments": "Json.NET is a popular high-performance JSON framework for .NET", // 1
"CompanyName": "Newtonsoft", // 2
"FileDescription": "Json.NET .NET 6.0", // 3
"FileVersion": "13.0.3.27908", // 4
"InternalName": "Newtonsoft.Json.dll", // 5
"LegalCopyright": "Copyright © James Newton-King 2008", // 6
"LegalTrademarks": "", // 7 (empty value actually exists in the string table)
"OriginalFilename": "Newtonsoft.Json.dll", // 8
"ProductName": "Json.NET", // 9
"ProductVersion": "13.0.3+0a2e291c0d9c0c7675d445703e51750363a549ef", // 10
"Assembly Version": "13.0.0.0", // 11
},
},
{
name: "humanizer",
path: "/app/Humanizer.dll",
fixture: "image-net8-app",
wantCLR: true,
wantVR: map[string]string{
"Comments": "A micro-framework that turns your normal strings, type names, enum fields, date fields ETC into a human friendly format",
"CompanyName": "Mehdi Khalili, Claire Novotny",
"FileDescription": "Humanizer",
"FileVersion": "2.14.1.48190",
"InternalName": "Humanizer.dll",
"LegalCopyright": "Copyright © .NET Foundation and Contributors",
"OriginalFilename": "Humanizer.dll",
"ProductName": "Humanizer (net6.0)",
"ProductVersion": "2.14.1+3ebc38de58",
"Assembly Version": "2.14.0.0",
},
wantErr: require.NoError,
},
{
name: "dotnetapp",
path: "/app/dotnetapp.dll",
fixture: "image-net8-app",
wantCLR: true,
wantVR: map[string]string{
"CompanyName": "dotnetapp",
"FileDescription": "dotnetapp",
"FileVersion": "1.0.0.0",
"InternalName": "dotnetapp.dll",
"LegalCopyright": " ",
"OriginalFilename": "dotnetapp.dll",
"ProductName": "dotnetapp",
"ProductVersion": "1.0.0",
"Assembly Version": "1.0.0.0",
},
wantErr: require.NoError,
},
{
name: "jruby",
path: "/app/jruby_windows_9_3_15_0.exe",
fixture: "image-net8-app",
wantCLR: false, // important!
wantVR: map[string]string{
"CompanyName": "JRuby Dev Team",
"FileDescription": "JRuby",
"FileVersion": "9.3.15.0",
"InternalName": "jruby",
"LegalCopyright": "JRuby Dev Team",
"OriginalFilename": "jruby_windows-x32_9_3_15_0.exe",
"ProductName": "JRuby",
"ProductVersion": "9.3.15.0",
},
wantErr: require.NoError,
},
{
name: "single file deployment",
path: "/app/dotnetapp.exe",
fixture: "image-net8-app-single-file",
// single file deployment does not have CLR metadata embedded in the COM descriptor. Instead we need
// to look for evidence of the CLR in other resources directory names, specifically for "CLRDEBUGINFO".
wantCLR: true,
wantVR: map[string]string{
"CompanyName": "dotnetapp",
"FileDescription": "dotnetapp",
"FileVersion": "1.0.0.0",
"InternalName": "dotnetapp.dll",
"LegalCopyright": " ",
"OriginalFilename": "dotnetapp.dll",
"ProductName": "dotnetapp",
"ProductVersion": "1.0.0",
"Assembly Version": "1.0.0.0",
},
wantErr: require.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = require.NoError
}
reader := fixtureFile(t, tt.fixture, tt.path)
got, err := getLogicalDotnetPE(reader)
tt.wantErr(t, err)
if err != nil {
return
}
if d := cmp.Diff(tt.wantVR, got.VersionResources); d != "" {
t.Errorf("unexpected version resources (-want +got): %s", d)
}
assert.Equal(t, tt.wantCLR, got.CLR.hasEvidenceOfCLR())
})
}
}
func fixtureFile(t *testing.T, fixture, path string) file.LocationReadCloser {
img := imagetest.GetFixtureImage(t, "docker-archive", fixture)
s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
Reference: fixture,
})
r, err := s.FileResolver(source.SquashedScope)
require.NoError(t, err)
locs, err := r.FilesByPath(path)
require.NoError(t, err)
require.Len(t, locs, 1)
loc := locs[0]
reader, err := r.FileContentsByLocation(loc)
require.NoError(t, err)
return file.NewLocationReadCloser(loc, reader)
}

View File

@ -1,2 +1,3 @@
!*.dll
!*.exe
/cache

View File

@ -0,0 +1,21 @@
FINGERPRINT_FILE=cache.fingerprint
.DEFAULT_GOAL := fixtures
# requirement 1: 'fixtures' goal to generate any and all test fixtures
fixtures:
@echo "nothing to do"
# requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted
fingerprint: $(FINGERPRINT_FILE)
# requirement 3: we always need to recalculate the fingerprint based on source regardless of any existing fingerprint
.PHONY: $(FINGERPRINT_FILE)
$(FINGERPRINT_FILE):
@find Makefile **/Dockerfile **/src/** -type f -exec sha256sum {} \; | sort -k2 > $(FINGERPRINT_FILE)
@#cat $(FINGERPRINT_FILE) | sha256sum | awk '{print $$1}'
# requirement 4: 'clean' goal to remove all generated test fixtures
clean:
rm -f $(FINGERPRINT_FILE)

View File

@ -0,0 +1 @@
/app

View File

@ -0,0 +1,22 @@
# This is the same as the net8-app image, but without the .deps.json file.
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:8.0-alpine@sha256:7d3a75ca5c8ac4679908ef7a2591b9bc257c62bd530167de32bba105148bb7be AS build
ARG RUNTIME=win-x64
WORKDIR /src
# copy csproj and restore as distinct layers
COPY src/*.csproj .
COPY src/packages.lock.json .
RUN dotnet restore -r $RUNTIME --verbosity normal --locked-mode
# copy and publish app and libraries
COPY src/ .
RUN dotnet publish -r $RUNTIME --no-restore -o /app
# important!
RUN rm /app/*.deps.json
FROM busybox
WORKDIR /app
COPY --from=build /app .
# just a nice to have for later...
#COPY --from=build /src/packages.lock.json .

View File

@ -0,0 +1,31 @@
using System;
using System.Net;
using System.Runtime.InteropServices;
using static System.Console;
WriteLine("Runtime and Environment Information");
// OS and .NET information
WriteLine($"{nameof(RuntimeInformation.OSArchitecture)}: {RuntimeInformation.OSArchitecture}");
WriteLine($"{nameof(RuntimeInformation.OSDescription)}: {RuntimeInformation.OSDescription}");
WriteLine($"{nameof(RuntimeInformation.FrameworkDescription)}: {RuntimeInformation.FrameworkDescription}");
WriteLine();
// Environment information
WriteLine($"{nameof(Environment.UserName)}: {Environment.UserName}");
WriteLine($"HostName: {Dns.GetHostName()}");
WriteLine($"{nameof(Environment.ProcessorCount)}: {Environment.ProcessorCount}");
WriteLine();
// Memory information
WriteLine($"Available Memory (GC): {GetInBestUnit(GC.GetGCMemoryInfo().TotalAvailableMemoryBytes)}");
string GetInBestUnit(long size)
{
const double Mebi = 1024 * 1024;
const double Gibi = Mebi * 1024;
if (size < Mebi) return $"{size} bytes";
if (size < Gibi) return $"{size / Mebi:F} MiB";
return $"{size / Gibi:F} GiB";
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Humanizer" Version="2.14.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,459 @@
{
"version": 1,
"dependencies": {
"net8.0": {
"Humanizer": {
"type": "Direct",
"requested": "[2.14.1, )",
"resolved": "2.14.1",
"contentHash": "/FUTD3cEceAAmJSCPN9+J+VhGwmL/C12jvwlyM1DFXShEMsBzvLzLqSrJ2rb+k/W2znKw7JyflZgZpyE+tI7lA==",
"dependencies": {
"Humanizer.Core.af": "2.14.1",
"Humanizer.Core.ar": "2.14.1",
"Humanizer.Core.az": "2.14.1",
"Humanizer.Core.bg": "2.14.1",
"Humanizer.Core.bn-BD": "2.14.1",
"Humanizer.Core.cs": "2.14.1",
"Humanizer.Core.da": "2.14.1",
"Humanizer.Core.de": "2.14.1",
"Humanizer.Core.el": "2.14.1",
"Humanizer.Core.es": "2.14.1",
"Humanizer.Core.fa": "2.14.1",
"Humanizer.Core.fi-FI": "2.14.1",
"Humanizer.Core.fr": "2.14.1",
"Humanizer.Core.fr-BE": "2.14.1",
"Humanizer.Core.he": "2.14.1",
"Humanizer.Core.hr": "2.14.1",
"Humanizer.Core.hu": "2.14.1",
"Humanizer.Core.hy": "2.14.1",
"Humanizer.Core.id": "2.14.1",
"Humanizer.Core.is": "2.14.1",
"Humanizer.Core.it": "2.14.1",
"Humanizer.Core.ja": "2.14.1",
"Humanizer.Core.ko-KR": "2.14.1",
"Humanizer.Core.ku": "2.14.1",
"Humanizer.Core.lv": "2.14.1",
"Humanizer.Core.ms-MY": "2.14.1",
"Humanizer.Core.mt": "2.14.1",
"Humanizer.Core.nb": "2.14.1",
"Humanizer.Core.nb-NO": "2.14.1",
"Humanizer.Core.nl": "2.14.1",
"Humanizer.Core.pl": "2.14.1",
"Humanizer.Core.pt": "2.14.1",
"Humanizer.Core.ro": "2.14.1",
"Humanizer.Core.ru": "2.14.1",
"Humanizer.Core.sk": "2.14.1",
"Humanizer.Core.sl": "2.14.1",
"Humanizer.Core.sr": "2.14.1",
"Humanizer.Core.sr-Latn": "2.14.1",
"Humanizer.Core.sv": "2.14.1",
"Humanizer.Core.th-TH": "2.14.1",
"Humanizer.Core.tr": "2.14.1",
"Humanizer.Core.uk": "2.14.1",
"Humanizer.Core.uz-Cyrl-UZ": "2.14.1",
"Humanizer.Core.uz-Latn-UZ": "2.14.1",
"Humanizer.Core.vi": "2.14.1",
"Humanizer.Core.zh-CN": "2.14.1",
"Humanizer.Core.zh-Hans": "2.14.1",
"Humanizer.Core.zh-Hant": "2.14.1"
}
},
"Newtonsoft.Json": {
"type": "Direct",
"requested": "[13.0.3, )",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"Humanizer.Core": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
},
"Humanizer.Core.af": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "BoQHyu5le+xxKOw+/AUM7CLXneM/Bh3++0qh1u0+D95n6f9eGt9kNc8LcAHLIOwId7Sd5hiAaaav0Nimj3peNw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ar": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "3d1V10LDtmqg5bZjWkA/EkmGFeSfNBcyCH+TiHcHP+HGQQmRq3eBaLcLnOJbVQVn3Z6Ak8GOte4RX4kVCxQlFA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.az": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "8Z/tp9PdHr/K2Stve2Qs/7uqWPWLUK9D8sOZDNzyv42e20bSoJkHFn7SFoxhmaoVLJwku2jp6P7HuwrfkrP18Q==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.bg": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "S+hIEHicrOcbV2TBtyoPp1AVIGsBzlarOGThhQYCnP6QzEYo/5imtok6LMmhZeTnBFoKhM8yJqRfvJ5yqVQKSQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.bn-BD": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "U3bfj90tnUDRKlL1ZFlzhCHoVgpTcqUlTQxjvGCaFKb+734TTu3nkHUWVZltA1E/swTvimo/aXLtkxnLFrc0EQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.cs": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "jWrQkiCTy3L2u1T86cFkgijX6k7hoB0pdcFMWYaSZnm6rvG/XJE40tfhYyKhYYgIc1x9P2GO5AC7xXvFnFdqMQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.da": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "5o0rJyE/2wWUUphC79rgYDnif/21MKTTx9LIzRVz9cjCIVFrJ2bDyR2gapvI9D6fjoyvD1NAfkN18SHBsO8S9g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.de": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "9JD/p+rqjb8f5RdZ3aEJqbjMYkbk4VFii2QDnnOdNo6ywEfg/A5YeOQ55CaBJmy7KvV4tOK4+qHJnX/tg3Z54A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.el": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Xmv6sTL5mqjOWGGpqY7bvbfK5RngaUHSa8fYDGSLyxY9mGdNbDcasnRnMOvi0SxJS9gAqBCn21Xi90n2SHZbFA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.es": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "e//OIAeMB7pjBV1HqqI4pM2Bcw3Jwgpyz9G5Fi4c+RJvhqFwztoWxW57PzTnNJE2lbhGGLQZihFZjsbTUsbczA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fa": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "nzDOj1x0NgjXMjsQxrET21t1FbdoRYujzbmZoR8u8ou5CBWY1UNca0j6n/PEJR/iUbt4IxstpszRy41wL/BrpA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fi-FI": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Vnxxx4LUhp3AzowYi6lZLAA9Lh8UqkdwRh4IE2qDXiVpbo08rSbokATaEzFS+o+/jCNZBmoyyyph3vgmcSzhhQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "2p4g0BYNzFS3u9SOIDByp2VClYKO0K1ecDV4BkB9EYdEPWfFODYnF+8CH8LpUrpxL2TuWo2fiFx/4Jcmrnkbpg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fr-BE": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "o6R3SerxCRn5Ij8nCihDNMGXlaJ/1AqefteAssgmU2qXYlSAGdhxmnrQAXZUDlE4YWt/XQ6VkNLtH7oMqsSPFQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.he": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "FPsAhy7Iw6hb+ZitLgYC26xNcgGAHXb0V823yFAzcyoL5ozM+DCJtYfDPYiOpsJhEZmKFTM9No0jUn1M89WGvg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "chnaD89yOlST142AMkAKLuzRcV5df3yyhDyRU5rypDiqrq2HN8y1UR3h1IicEAEtXLoOEQyjSAkAQ6QuXkn7aw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hu": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "hAfnaoF9LTGU/CmFdbnvugN4tIs8ppevVMe3e5bD24+tuKsggMc5hYta9aiydI8JH9JnuVmxvNI4DJee1tK05A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hy": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "sVIKxOiSBUb4gStRHo9XwwAg9w7TNvAXbjy176gyTtaTiZkcjr9aCPziUlYAF07oNz6SdwdC2mwJBGgvZ0Sl2g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.id": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "4Zl3GTvk3a49Ia/WDNQ97eCupjjQRs2iCIZEQdmkiqyaLWttfb+cYXDMGthP42nufUL0SRsvBctN67oSpnXtsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.is": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "R67A9j/nNgcWzU7gZy1AJ07ABSLvogRbqOWvfRDn4q6hNdbg/mjGjZBp4qCTPnB2mHQQTCKo3oeCUayBCNIBCw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.it": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "jYxGeN4XIKHVND02FZ+Woir3CUTyBhLsqxu9iqR/9BISArkMf1Px6i5pRZnvq4fc5Zn1qw71GKKoCaHDJBsLFw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ja": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "TM3ablFNoYx4cYJybmRgpDioHpiKSD7q0QtMrmpsqwtiiEsdW5zz/q4PolwAczFnvrKpN6nBXdjnPPKVet93ng==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ko-KR": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "CtvwvK941k/U0r8PGdEuBEMdW6jv/rBiA9tUhakC7Zd2rA/HCnDcbr1DiNZ+/tRshnhzxy/qwmpY8h4qcAYCtQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ku": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "vHmzXcVMe+LNrF9txpdHzpG7XJX65SiN9GQd/Zkt6gsGIIEeECHrkwCN5Jnlkddw2M/b0HS4SNxdR1GrSn7uCA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.lv": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "E1/KUVnYBS1bdOTMNDD7LV/jdoZv/fbWTLPtvwdMtSdqLyRTllv6PGM9xVQoFDYlpvVGtEl/09glCojPHw8ffA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ms-MY": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "vX8oq9HnYmAF7bek4aGgGFJficHDRTLgp/EOiPv9mBZq0i4SA96qVMYSjJ2YTaxs7Eljqit7pfpE2nmBhY5Fnw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.mt": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "pEgTBzUI9hzemF7xrIZigl44LidTUhNu4x/P6M9sAwZjkUF0mMkbpxKkaasOql7lLafKrnszs0xFfaxQyzeuZQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nb": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "mbs3m6JJq53ssLqVPxNfqSdTxAcZN3njlG8yhJVx83XVedpTe1ECK9aCa8FKVOXv93Gl+yRHF82Hw9T9LWv2hw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nb-NO": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "AsJxrrVYmIMbKDGe8W6Z6//wKv9dhWH7RsTcEHSr4tQt/80pcNvLi0hgD3fqfTtg0tWKtgch2cLf4prorEV+5A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "24b0OUdzJxfoqiHPCtYnR5Y4l/s4Oh7KW7uDp+qX25NMAHLCGog2eRfA7p2kRJp8LvnynwwQxm2p534V9m55wQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.pl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "17mJNYaBssENVZyQHduiq+bvdXS0nhZJGEXtPKoMhKv3GD//WO0mEfd9wjEBsWCSmWI7bjRqhCidxzN+YtJmsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.pt": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "8HB8qavcVp2la1GJX6t+G9nDYtylPKzyhxr9LAooIei9MnQvNsjEiIE4QvHoeDZ4weuQ9CsPg1c211XUMVEZ4A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ro": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "psXNOcA6R8fSHoQYhpBTtTTYiOk8OBoN3PKCEDgsJKIyeY5xuK81IBdGi77qGZMu/OwBRQjQCBMtPJb0f4O1+A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ru": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "zm245xUWrajSN2t9H7BTf84/2APbUkKlUJpcdgsvTdAysr1ag9fi1APu6JEok39RRBXDfNRVZHawQ/U8X0pSvQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sk": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Ncw24Vf3ioRnbU4MsMFHafkyYi8JOnTqvK741GftlQvAbULBoTz2+e7JByOaasqeSi0KfTXeegJO+5Wk1c0Mbw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "l8sUy4ciAIbVThWNL0atzTS2HWtv8qJrsGWNlqrEKmPwA4SdKolSqnTes9V89fyZTc2Q43jK8fgzVE2C7t009A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rnNvhpkOrWEymy7R/MiFv7uef8YO5HuXDyvojZ7JpijHWA5dXuVXooCOiA/3E93fYa3pxDuG2OQe4M/olXbQ7w==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sr-Latn": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "nuy/ykpk974F8ItoQMS00kJPr2dFNjOSjgzCwfysbu7+gjqHmbLcYs7G4kshLwdA4AsVncxp99LYeJgoh1JF5g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sv": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "E53+tpAG0RCp+cSSI7TfBPC+NnsEqUuoSV0sU+rWRXWr9MbRWx1+Zj02XMojqjGzHjjOrBFBBio6m74seFl0AA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.th-TH": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "eSevlJtvs1r4vQarNPfZ2kKDp/xMhuD00tVVzRXkSh1IAZbBJI/x2ydxUOwfK9bEwEp+YjvL1Djx2+kw7ziu7g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.tr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rQ8N+o7yFcFqdbtu1mmbrXFi8TQ+uy+fVH9OPI0CI3Cu1om5hUU/GOMC3hXsTCI6d79y4XX+0HbnD7FT5khegA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uk": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "2uEfujwXKNm6bdpukaLtEJD+04uUtQD65nSGCetA1fYNizItEaIBUboNfr3GzJxSMQotNwGVM3+nSn8jTd0VSg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uz-Cyrl-UZ": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "TD3ME2sprAvFqk9tkWrvSKx5XxEMlAn1sjk+cYClSWZlIMhQQ2Bp/w0VjX1Kc5oeKjxRAnR7vFcLUFLiZIDk9Q==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uz-Latn-UZ": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "/kHAoF4g0GahnugZiEMpaHlxb+W6jCEbWIdsq9/I1k48ULOsl/J0pxZj93lXC3omGzVF1BTVIeAtv5fW06Phsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.vi": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rsQNh9rmHMBtnsUUlJbShMsIMGflZtPmrMM6JNDw20nhsvqfrdcoDD8cMnLAbuSovtc3dP+swRmLQzKmXDTVPA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-CN": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "uH2dWhrgugkCjDmduLdAFO9w1Mo0q07EuvM0QiIZCVm6FMCu/lGv2fpMu4GX+4HLZ6h5T2Pg9FIdDLCPN2a67w==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-Hans": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "WH6IhJ8V1UBG7rZXQk3dZUoP2gsi8a0WkL8xL0sN6WGiv695s8nVcmab9tWz20ySQbuzp0UkSxUQFi5jJHIpOQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-Hant": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "VIXB7HCUC34OoaGnO3HJVtSv2/wljPhjV7eKH4+TFPgQdJj2lvHNKY41Dtg0Bphu7X5UaXFR4zrYYyo+GNOjbA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
}
},
"net8.0/win-x64": {}
}
}

View File

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

View File

@ -0,0 +1,21 @@
# This is the same as the net8-app image, however, the .NET runtime is copied to the target directory tree. There
# is no bundling to include the runtime within a single archive/binary.
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:8.0-alpine@sha256:7d3a75ca5c8ac4679908ef7a2591b9bc257c62bd530167de32bba105148bb7be AS build
ARG RUNTIME=win-x64
WORKDIR /src
# copy csproj and restore as distinct layers
COPY src/*.csproj .
COPY src/packages.lock.json .
RUN dotnet restore -r $RUNTIME --verbosity normal --locked-mode
# copy and publish app and libraries (self-contained!)
COPY src/ .
RUN dotnet publish -r $RUNTIME --self-contained --no-restore -o /app
FROM busybox
WORKDIR /app
COPY --from=build /app .
# just a nice to have for later...
#COPY --from=build /src/packages.lock.json .

View File

@ -0,0 +1,31 @@
using System;
using System.Net;
using System.Runtime.InteropServices;
using static System.Console;
WriteLine("Runtime and Environment Information");
// OS and .NET information
WriteLine($"{nameof(RuntimeInformation.OSArchitecture)}: {RuntimeInformation.OSArchitecture}");
WriteLine($"{nameof(RuntimeInformation.OSDescription)}: {RuntimeInformation.OSDescription}");
WriteLine($"{nameof(RuntimeInformation.FrameworkDescription)}: {RuntimeInformation.FrameworkDescription}");
WriteLine();
// Environment information
WriteLine($"{nameof(Environment.UserName)}: {Environment.UserName}");
WriteLine($"HostName: {Dns.GetHostName()}");
WriteLine($"{nameof(Environment.ProcessorCount)}: {Environment.ProcessorCount}");
WriteLine();
// Memory information
WriteLine($"Available Memory (GC): {GetInBestUnit(GC.GetGCMemoryInfo().TotalAvailableMemoryBytes)}");
string GetInBestUnit(long size)
{
const double Mebi = 1024 * 1024;
const double Gibi = Mebi * 1024;
if (size < Mebi) return $"{size} bytes";
if (size < Gibi) return $"{size / Mebi:F} MiB";
return $"{size / Gibi:F} GiB";
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Humanizer" Version="2.14.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,459 @@
{
"version": 1,
"dependencies": {
"net8.0": {
"Humanizer": {
"type": "Direct",
"requested": "[2.14.1, )",
"resolved": "2.14.1",
"contentHash": "/FUTD3cEceAAmJSCPN9+J+VhGwmL/C12jvwlyM1DFXShEMsBzvLzLqSrJ2rb+k/W2znKw7JyflZgZpyE+tI7lA==",
"dependencies": {
"Humanizer.Core.af": "2.14.1",
"Humanizer.Core.ar": "2.14.1",
"Humanizer.Core.az": "2.14.1",
"Humanizer.Core.bg": "2.14.1",
"Humanizer.Core.bn-BD": "2.14.1",
"Humanizer.Core.cs": "2.14.1",
"Humanizer.Core.da": "2.14.1",
"Humanizer.Core.de": "2.14.1",
"Humanizer.Core.el": "2.14.1",
"Humanizer.Core.es": "2.14.1",
"Humanizer.Core.fa": "2.14.1",
"Humanizer.Core.fi-FI": "2.14.1",
"Humanizer.Core.fr": "2.14.1",
"Humanizer.Core.fr-BE": "2.14.1",
"Humanizer.Core.he": "2.14.1",
"Humanizer.Core.hr": "2.14.1",
"Humanizer.Core.hu": "2.14.1",
"Humanizer.Core.hy": "2.14.1",
"Humanizer.Core.id": "2.14.1",
"Humanizer.Core.is": "2.14.1",
"Humanizer.Core.it": "2.14.1",
"Humanizer.Core.ja": "2.14.1",
"Humanizer.Core.ko-KR": "2.14.1",
"Humanizer.Core.ku": "2.14.1",
"Humanizer.Core.lv": "2.14.1",
"Humanizer.Core.ms-MY": "2.14.1",
"Humanizer.Core.mt": "2.14.1",
"Humanizer.Core.nb": "2.14.1",
"Humanizer.Core.nb-NO": "2.14.1",
"Humanizer.Core.nl": "2.14.1",
"Humanizer.Core.pl": "2.14.1",
"Humanizer.Core.pt": "2.14.1",
"Humanizer.Core.ro": "2.14.1",
"Humanizer.Core.ru": "2.14.1",
"Humanizer.Core.sk": "2.14.1",
"Humanizer.Core.sl": "2.14.1",
"Humanizer.Core.sr": "2.14.1",
"Humanizer.Core.sr-Latn": "2.14.1",
"Humanizer.Core.sv": "2.14.1",
"Humanizer.Core.th-TH": "2.14.1",
"Humanizer.Core.tr": "2.14.1",
"Humanizer.Core.uk": "2.14.1",
"Humanizer.Core.uz-Cyrl-UZ": "2.14.1",
"Humanizer.Core.uz-Latn-UZ": "2.14.1",
"Humanizer.Core.vi": "2.14.1",
"Humanizer.Core.zh-CN": "2.14.1",
"Humanizer.Core.zh-Hans": "2.14.1",
"Humanizer.Core.zh-Hant": "2.14.1"
}
},
"Newtonsoft.Json": {
"type": "Direct",
"requested": "[13.0.3, )",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"Humanizer.Core": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
},
"Humanizer.Core.af": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "BoQHyu5le+xxKOw+/AUM7CLXneM/Bh3++0qh1u0+D95n6f9eGt9kNc8LcAHLIOwId7Sd5hiAaaav0Nimj3peNw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ar": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "3d1V10LDtmqg5bZjWkA/EkmGFeSfNBcyCH+TiHcHP+HGQQmRq3eBaLcLnOJbVQVn3Z6Ak8GOte4RX4kVCxQlFA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.az": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "8Z/tp9PdHr/K2Stve2Qs/7uqWPWLUK9D8sOZDNzyv42e20bSoJkHFn7SFoxhmaoVLJwku2jp6P7HuwrfkrP18Q==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.bg": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "S+hIEHicrOcbV2TBtyoPp1AVIGsBzlarOGThhQYCnP6QzEYo/5imtok6LMmhZeTnBFoKhM8yJqRfvJ5yqVQKSQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.bn-BD": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "U3bfj90tnUDRKlL1ZFlzhCHoVgpTcqUlTQxjvGCaFKb+734TTu3nkHUWVZltA1E/swTvimo/aXLtkxnLFrc0EQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.cs": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "jWrQkiCTy3L2u1T86cFkgijX6k7hoB0pdcFMWYaSZnm6rvG/XJE40tfhYyKhYYgIc1x9P2GO5AC7xXvFnFdqMQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.da": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "5o0rJyE/2wWUUphC79rgYDnif/21MKTTx9LIzRVz9cjCIVFrJ2bDyR2gapvI9D6fjoyvD1NAfkN18SHBsO8S9g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.de": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "9JD/p+rqjb8f5RdZ3aEJqbjMYkbk4VFii2QDnnOdNo6ywEfg/A5YeOQ55CaBJmy7KvV4tOK4+qHJnX/tg3Z54A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.el": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Xmv6sTL5mqjOWGGpqY7bvbfK5RngaUHSa8fYDGSLyxY9mGdNbDcasnRnMOvi0SxJS9gAqBCn21Xi90n2SHZbFA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.es": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "e//OIAeMB7pjBV1HqqI4pM2Bcw3Jwgpyz9G5Fi4c+RJvhqFwztoWxW57PzTnNJE2lbhGGLQZihFZjsbTUsbczA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fa": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "nzDOj1x0NgjXMjsQxrET21t1FbdoRYujzbmZoR8u8ou5CBWY1UNca0j6n/PEJR/iUbt4IxstpszRy41wL/BrpA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fi-FI": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Vnxxx4LUhp3AzowYi6lZLAA9Lh8UqkdwRh4IE2qDXiVpbo08rSbokATaEzFS+o+/jCNZBmoyyyph3vgmcSzhhQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "2p4g0BYNzFS3u9SOIDByp2VClYKO0K1ecDV4BkB9EYdEPWfFODYnF+8CH8LpUrpxL2TuWo2fiFx/4Jcmrnkbpg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fr-BE": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "o6R3SerxCRn5Ij8nCihDNMGXlaJ/1AqefteAssgmU2qXYlSAGdhxmnrQAXZUDlE4YWt/XQ6VkNLtH7oMqsSPFQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.he": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "FPsAhy7Iw6hb+ZitLgYC26xNcgGAHXb0V823yFAzcyoL5ozM+DCJtYfDPYiOpsJhEZmKFTM9No0jUn1M89WGvg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "chnaD89yOlST142AMkAKLuzRcV5df3yyhDyRU5rypDiqrq2HN8y1UR3h1IicEAEtXLoOEQyjSAkAQ6QuXkn7aw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hu": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "hAfnaoF9LTGU/CmFdbnvugN4tIs8ppevVMe3e5bD24+tuKsggMc5hYta9aiydI8JH9JnuVmxvNI4DJee1tK05A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hy": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "sVIKxOiSBUb4gStRHo9XwwAg9w7TNvAXbjy176gyTtaTiZkcjr9aCPziUlYAF07oNz6SdwdC2mwJBGgvZ0Sl2g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.id": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "4Zl3GTvk3a49Ia/WDNQ97eCupjjQRs2iCIZEQdmkiqyaLWttfb+cYXDMGthP42nufUL0SRsvBctN67oSpnXtsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.is": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "R67A9j/nNgcWzU7gZy1AJ07ABSLvogRbqOWvfRDn4q6hNdbg/mjGjZBp4qCTPnB2mHQQTCKo3oeCUayBCNIBCw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.it": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "jYxGeN4XIKHVND02FZ+Woir3CUTyBhLsqxu9iqR/9BISArkMf1Px6i5pRZnvq4fc5Zn1qw71GKKoCaHDJBsLFw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ja": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "TM3ablFNoYx4cYJybmRgpDioHpiKSD7q0QtMrmpsqwtiiEsdW5zz/q4PolwAczFnvrKpN6nBXdjnPPKVet93ng==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ko-KR": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "CtvwvK941k/U0r8PGdEuBEMdW6jv/rBiA9tUhakC7Zd2rA/HCnDcbr1DiNZ+/tRshnhzxy/qwmpY8h4qcAYCtQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ku": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "vHmzXcVMe+LNrF9txpdHzpG7XJX65SiN9GQd/Zkt6gsGIIEeECHrkwCN5Jnlkddw2M/b0HS4SNxdR1GrSn7uCA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.lv": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "E1/KUVnYBS1bdOTMNDD7LV/jdoZv/fbWTLPtvwdMtSdqLyRTllv6PGM9xVQoFDYlpvVGtEl/09glCojPHw8ffA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ms-MY": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "vX8oq9HnYmAF7bek4aGgGFJficHDRTLgp/EOiPv9mBZq0i4SA96qVMYSjJ2YTaxs7Eljqit7pfpE2nmBhY5Fnw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.mt": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "pEgTBzUI9hzemF7xrIZigl44LidTUhNu4x/P6M9sAwZjkUF0mMkbpxKkaasOql7lLafKrnszs0xFfaxQyzeuZQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nb": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "mbs3m6JJq53ssLqVPxNfqSdTxAcZN3njlG8yhJVx83XVedpTe1ECK9aCa8FKVOXv93Gl+yRHF82Hw9T9LWv2hw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nb-NO": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "AsJxrrVYmIMbKDGe8W6Z6//wKv9dhWH7RsTcEHSr4tQt/80pcNvLi0hgD3fqfTtg0tWKtgch2cLf4prorEV+5A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "24b0OUdzJxfoqiHPCtYnR5Y4l/s4Oh7KW7uDp+qX25NMAHLCGog2eRfA7p2kRJp8LvnynwwQxm2p534V9m55wQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.pl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "17mJNYaBssENVZyQHduiq+bvdXS0nhZJGEXtPKoMhKv3GD//WO0mEfd9wjEBsWCSmWI7bjRqhCidxzN+YtJmsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.pt": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "8HB8qavcVp2la1GJX6t+G9nDYtylPKzyhxr9LAooIei9MnQvNsjEiIE4QvHoeDZ4weuQ9CsPg1c211XUMVEZ4A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ro": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "psXNOcA6R8fSHoQYhpBTtTTYiOk8OBoN3PKCEDgsJKIyeY5xuK81IBdGi77qGZMu/OwBRQjQCBMtPJb0f4O1+A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ru": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "zm245xUWrajSN2t9H7BTf84/2APbUkKlUJpcdgsvTdAysr1ag9fi1APu6JEok39RRBXDfNRVZHawQ/U8X0pSvQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sk": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Ncw24Vf3ioRnbU4MsMFHafkyYi8JOnTqvK741GftlQvAbULBoTz2+e7JByOaasqeSi0KfTXeegJO+5Wk1c0Mbw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "l8sUy4ciAIbVThWNL0atzTS2HWtv8qJrsGWNlqrEKmPwA4SdKolSqnTes9V89fyZTc2Q43jK8fgzVE2C7t009A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rnNvhpkOrWEymy7R/MiFv7uef8YO5HuXDyvojZ7JpijHWA5dXuVXooCOiA/3E93fYa3pxDuG2OQe4M/olXbQ7w==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sr-Latn": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "nuy/ykpk974F8ItoQMS00kJPr2dFNjOSjgzCwfysbu7+gjqHmbLcYs7G4kshLwdA4AsVncxp99LYeJgoh1JF5g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sv": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "E53+tpAG0RCp+cSSI7TfBPC+NnsEqUuoSV0sU+rWRXWr9MbRWx1+Zj02XMojqjGzHjjOrBFBBio6m74seFl0AA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.th-TH": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "eSevlJtvs1r4vQarNPfZ2kKDp/xMhuD00tVVzRXkSh1IAZbBJI/x2ydxUOwfK9bEwEp+YjvL1Djx2+kw7ziu7g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.tr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rQ8N+o7yFcFqdbtu1mmbrXFi8TQ+uy+fVH9OPI0CI3Cu1om5hUU/GOMC3hXsTCI6d79y4XX+0HbnD7FT5khegA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uk": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "2uEfujwXKNm6bdpukaLtEJD+04uUtQD65nSGCetA1fYNizItEaIBUboNfr3GzJxSMQotNwGVM3+nSn8jTd0VSg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uz-Cyrl-UZ": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "TD3ME2sprAvFqk9tkWrvSKx5XxEMlAn1sjk+cYClSWZlIMhQQ2Bp/w0VjX1Kc5oeKjxRAnR7vFcLUFLiZIDk9Q==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uz-Latn-UZ": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "/kHAoF4g0GahnugZiEMpaHlxb+W6jCEbWIdsq9/I1k48ULOsl/J0pxZj93lXC3omGzVF1BTVIeAtv5fW06Phsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.vi": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rsQNh9rmHMBtnsUUlJbShMsIMGflZtPmrMM6JNDw20nhsvqfrdcoDD8cMnLAbuSovtc3dP+swRmLQzKmXDTVPA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-CN": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "uH2dWhrgugkCjDmduLdAFO9w1Mo0q07EuvM0QiIZCVm6FMCu/lGv2fpMu4GX+4HLZ6h5T2Pg9FIdDLCPN2a67w==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-Hans": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "WH6IhJ8V1UBG7rZXQk3dZUoP2gsi8a0WkL8xL0sN6WGiv695s8nVcmab9tWz20ySQbuzp0UkSxUQFi5jJHIpOQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-Hant": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "VIXB7HCUC34OoaGnO3HJVtSv2/wljPhjV7eKH4+TFPgQdJj2lvHNKY41Dtg0Bphu7X5UaXFR4zrYYyo+GNOjbA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
}
},
"net8.0/win-x64": {}
}
}

View File

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

View File

@ -0,0 +1,20 @@
# This is the same as the net8-app image, however, the entire .NET runtime is compiled into a single binary, residing with the application.
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:8.0-alpine@sha256:7d3a75ca5c8ac4679908ef7a2591b9bc257c62bd530167de32bba105148bb7be AS build
ARG RUNTIME=win-x64
WORKDIR /src
# copy csproj and restore as distinct layers
COPY src/*.csproj .
COPY src/packages.lock.json .
RUN dotnet restore -r $RUNTIME --verbosity normal --locked-mode
# copy and publish app and libraries (single file)
COPY src/ .
RUN dotnet publish -r $RUNTIME -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:PublishTrimmed=true --no-restore -o /app
FROM busybox
WORKDIR /app
COPY --from=build /app .
# just a nice to have for later...
#COPY --from=build /src/packages.lock.json .

View File

@ -0,0 +1,48 @@
using System;
using Humanizer;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace IndirectDependencyExample
{
class Program
{
static void Main(string[] args)
{
string runtimeInfo = "hello world!\n";
Console.WriteLine(runtimeInfo);
Console.WriteLine($"\"this_is_a_test\" to title case: {"this_is_a_test".Humanize(LetterCasing.Title)}");
const string jsonString = @"
{
""message"": ""Hello from JSON!"",
""details"": {
""timestamp"": ""2025-03-26T12:00:00Z"",
""version"": ""1.0.0"",
""metadata"": {
""author"": ""Claude"",
""environment"": ""Development""
}
},
""items"": [
{
""id"": 1,
""name"": ""Item One""
},
{
""id"": 2,
""name"": ""Item Two""
}
]
}";
JObject jsonObject = JObject.Parse(jsonString);
string message = (string)jsonObject["message"];
Console.WriteLine($"Message: {message}");
}
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Humanizer" Version="2.14.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,459 @@
{
"version": 1,
"dependencies": {
"net8.0": {
"Humanizer": {
"type": "Direct",
"requested": "[2.14.1, )",
"resolved": "2.14.1",
"contentHash": "/FUTD3cEceAAmJSCPN9+J+VhGwmL/C12jvwlyM1DFXShEMsBzvLzLqSrJ2rb+k/W2znKw7JyflZgZpyE+tI7lA==",
"dependencies": {
"Humanizer.Core.af": "2.14.1",
"Humanizer.Core.ar": "2.14.1",
"Humanizer.Core.az": "2.14.1",
"Humanizer.Core.bg": "2.14.1",
"Humanizer.Core.bn-BD": "2.14.1",
"Humanizer.Core.cs": "2.14.1",
"Humanizer.Core.da": "2.14.1",
"Humanizer.Core.de": "2.14.1",
"Humanizer.Core.el": "2.14.1",
"Humanizer.Core.es": "2.14.1",
"Humanizer.Core.fa": "2.14.1",
"Humanizer.Core.fi-FI": "2.14.1",
"Humanizer.Core.fr": "2.14.1",
"Humanizer.Core.fr-BE": "2.14.1",
"Humanizer.Core.he": "2.14.1",
"Humanizer.Core.hr": "2.14.1",
"Humanizer.Core.hu": "2.14.1",
"Humanizer.Core.hy": "2.14.1",
"Humanizer.Core.id": "2.14.1",
"Humanizer.Core.is": "2.14.1",
"Humanizer.Core.it": "2.14.1",
"Humanizer.Core.ja": "2.14.1",
"Humanizer.Core.ko-KR": "2.14.1",
"Humanizer.Core.ku": "2.14.1",
"Humanizer.Core.lv": "2.14.1",
"Humanizer.Core.ms-MY": "2.14.1",
"Humanizer.Core.mt": "2.14.1",
"Humanizer.Core.nb": "2.14.1",
"Humanizer.Core.nb-NO": "2.14.1",
"Humanizer.Core.nl": "2.14.1",
"Humanizer.Core.pl": "2.14.1",
"Humanizer.Core.pt": "2.14.1",
"Humanizer.Core.ro": "2.14.1",
"Humanizer.Core.ru": "2.14.1",
"Humanizer.Core.sk": "2.14.1",
"Humanizer.Core.sl": "2.14.1",
"Humanizer.Core.sr": "2.14.1",
"Humanizer.Core.sr-Latn": "2.14.1",
"Humanizer.Core.sv": "2.14.1",
"Humanizer.Core.th-TH": "2.14.1",
"Humanizer.Core.tr": "2.14.1",
"Humanizer.Core.uk": "2.14.1",
"Humanizer.Core.uz-Cyrl-UZ": "2.14.1",
"Humanizer.Core.uz-Latn-UZ": "2.14.1",
"Humanizer.Core.vi": "2.14.1",
"Humanizer.Core.zh-CN": "2.14.1",
"Humanizer.Core.zh-Hans": "2.14.1",
"Humanizer.Core.zh-Hant": "2.14.1"
}
},
"Newtonsoft.Json": {
"type": "Direct",
"requested": "[13.0.3, )",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"Humanizer.Core": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
},
"Humanizer.Core.af": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "BoQHyu5le+xxKOw+/AUM7CLXneM/Bh3++0qh1u0+D95n6f9eGt9kNc8LcAHLIOwId7Sd5hiAaaav0Nimj3peNw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ar": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "3d1V10LDtmqg5bZjWkA/EkmGFeSfNBcyCH+TiHcHP+HGQQmRq3eBaLcLnOJbVQVn3Z6Ak8GOte4RX4kVCxQlFA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.az": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "8Z/tp9PdHr/K2Stve2Qs/7uqWPWLUK9D8sOZDNzyv42e20bSoJkHFn7SFoxhmaoVLJwku2jp6P7HuwrfkrP18Q==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.bg": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "S+hIEHicrOcbV2TBtyoPp1AVIGsBzlarOGThhQYCnP6QzEYo/5imtok6LMmhZeTnBFoKhM8yJqRfvJ5yqVQKSQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.bn-BD": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "U3bfj90tnUDRKlL1ZFlzhCHoVgpTcqUlTQxjvGCaFKb+734TTu3nkHUWVZltA1E/swTvimo/aXLtkxnLFrc0EQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.cs": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "jWrQkiCTy3L2u1T86cFkgijX6k7hoB0pdcFMWYaSZnm6rvG/XJE40tfhYyKhYYgIc1x9P2GO5AC7xXvFnFdqMQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.da": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "5o0rJyE/2wWUUphC79rgYDnif/21MKTTx9LIzRVz9cjCIVFrJ2bDyR2gapvI9D6fjoyvD1NAfkN18SHBsO8S9g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.de": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "9JD/p+rqjb8f5RdZ3aEJqbjMYkbk4VFii2QDnnOdNo6ywEfg/A5YeOQ55CaBJmy7KvV4tOK4+qHJnX/tg3Z54A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.el": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Xmv6sTL5mqjOWGGpqY7bvbfK5RngaUHSa8fYDGSLyxY9mGdNbDcasnRnMOvi0SxJS9gAqBCn21Xi90n2SHZbFA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.es": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "e//OIAeMB7pjBV1HqqI4pM2Bcw3Jwgpyz9G5Fi4c+RJvhqFwztoWxW57PzTnNJE2lbhGGLQZihFZjsbTUsbczA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fa": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "nzDOj1x0NgjXMjsQxrET21t1FbdoRYujzbmZoR8u8ou5CBWY1UNca0j6n/PEJR/iUbt4IxstpszRy41wL/BrpA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fi-FI": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Vnxxx4LUhp3AzowYi6lZLAA9Lh8UqkdwRh4IE2qDXiVpbo08rSbokATaEzFS+o+/jCNZBmoyyyph3vgmcSzhhQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "2p4g0BYNzFS3u9SOIDByp2VClYKO0K1ecDV4BkB9EYdEPWfFODYnF+8CH8LpUrpxL2TuWo2fiFx/4Jcmrnkbpg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fr-BE": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "o6R3SerxCRn5Ij8nCihDNMGXlaJ/1AqefteAssgmU2qXYlSAGdhxmnrQAXZUDlE4YWt/XQ6VkNLtH7oMqsSPFQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.he": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "FPsAhy7Iw6hb+ZitLgYC26xNcgGAHXb0V823yFAzcyoL5ozM+DCJtYfDPYiOpsJhEZmKFTM9No0jUn1M89WGvg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "chnaD89yOlST142AMkAKLuzRcV5df3yyhDyRU5rypDiqrq2HN8y1UR3h1IicEAEtXLoOEQyjSAkAQ6QuXkn7aw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hu": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "hAfnaoF9LTGU/CmFdbnvugN4tIs8ppevVMe3e5bD24+tuKsggMc5hYta9aiydI8JH9JnuVmxvNI4DJee1tK05A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hy": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "sVIKxOiSBUb4gStRHo9XwwAg9w7TNvAXbjy176gyTtaTiZkcjr9aCPziUlYAF07oNz6SdwdC2mwJBGgvZ0Sl2g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.id": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "4Zl3GTvk3a49Ia/WDNQ97eCupjjQRs2iCIZEQdmkiqyaLWttfb+cYXDMGthP42nufUL0SRsvBctN67oSpnXtsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.is": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "R67A9j/nNgcWzU7gZy1AJ07ABSLvogRbqOWvfRDn4q6hNdbg/mjGjZBp4qCTPnB2mHQQTCKo3oeCUayBCNIBCw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.it": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "jYxGeN4XIKHVND02FZ+Woir3CUTyBhLsqxu9iqR/9BISArkMf1Px6i5pRZnvq4fc5Zn1qw71GKKoCaHDJBsLFw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ja": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "TM3ablFNoYx4cYJybmRgpDioHpiKSD7q0QtMrmpsqwtiiEsdW5zz/q4PolwAczFnvrKpN6nBXdjnPPKVet93ng==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ko-KR": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "CtvwvK941k/U0r8PGdEuBEMdW6jv/rBiA9tUhakC7Zd2rA/HCnDcbr1DiNZ+/tRshnhzxy/qwmpY8h4qcAYCtQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ku": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "vHmzXcVMe+LNrF9txpdHzpG7XJX65SiN9GQd/Zkt6gsGIIEeECHrkwCN5Jnlkddw2M/b0HS4SNxdR1GrSn7uCA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.lv": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "E1/KUVnYBS1bdOTMNDD7LV/jdoZv/fbWTLPtvwdMtSdqLyRTllv6PGM9xVQoFDYlpvVGtEl/09glCojPHw8ffA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ms-MY": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "vX8oq9HnYmAF7bek4aGgGFJficHDRTLgp/EOiPv9mBZq0i4SA96qVMYSjJ2YTaxs7Eljqit7pfpE2nmBhY5Fnw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.mt": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "pEgTBzUI9hzemF7xrIZigl44LidTUhNu4x/P6M9sAwZjkUF0mMkbpxKkaasOql7lLafKrnszs0xFfaxQyzeuZQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nb": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "mbs3m6JJq53ssLqVPxNfqSdTxAcZN3njlG8yhJVx83XVedpTe1ECK9aCa8FKVOXv93Gl+yRHF82Hw9T9LWv2hw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nb-NO": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "AsJxrrVYmIMbKDGe8W6Z6//wKv9dhWH7RsTcEHSr4tQt/80pcNvLi0hgD3fqfTtg0tWKtgch2cLf4prorEV+5A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "24b0OUdzJxfoqiHPCtYnR5Y4l/s4Oh7KW7uDp+qX25NMAHLCGog2eRfA7p2kRJp8LvnynwwQxm2p534V9m55wQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.pl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "17mJNYaBssENVZyQHduiq+bvdXS0nhZJGEXtPKoMhKv3GD//WO0mEfd9wjEBsWCSmWI7bjRqhCidxzN+YtJmsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.pt": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "8HB8qavcVp2la1GJX6t+G9nDYtylPKzyhxr9LAooIei9MnQvNsjEiIE4QvHoeDZ4weuQ9CsPg1c211XUMVEZ4A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ro": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "psXNOcA6R8fSHoQYhpBTtTTYiOk8OBoN3PKCEDgsJKIyeY5xuK81IBdGi77qGZMu/OwBRQjQCBMtPJb0f4O1+A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ru": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "zm245xUWrajSN2t9H7BTf84/2APbUkKlUJpcdgsvTdAysr1ag9fi1APu6JEok39RRBXDfNRVZHawQ/U8X0pSvQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sk": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Ncw24Vf3ioRnbU4MsMFHafkyYi8JOnTqvK741GftlQvAbULBoTz2+e7JByOaasqeSi0KfTXeegJO+5Wk1c0Mbw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "l8sUy4ciAIbVThWNL0atzTS2HWtv8qJrsGWNlqrEKmPwA4SdKolSqnTes9V89fyZTc2Q43jK8fgzVE2C7t009A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rnNvhpkOrWEymy7R/MiFv7uef8YO5HuXDyvojZ7JpijHWA5dXuVXooCOiA/3E93fYa3pxDuG2OQe4M/olXbQ7w==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sr-Latn": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "nuy/ykpk974F8ItoQMS00kJPr2dFNjOSjgzCwfysbu7+gjqHmbLcYs7G4kshLwdA4AsVncxp99LYeJgoh1JF5g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sv": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "E53+tpAG0RCp+cSSI7TfBPC+NnsEqUuoSV0sU+rWRXWr9MbRWx1+Zj02XMojqjGzHjjOrBFBBio6m74seFl0AA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.th-TH": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "eSevlJtvs1r4vQarNPfZ2kKDp/xMhuD00tVVzRXkSh1IAZbBJI/x2ydxUOwfK9bEwEp+YjvL1Djx2+kw7ziu7g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.tr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rQ8N+o7yFcFqdbtu1mmbrXFi8TQ+uy+fVH9OPI0CI3Cu1om5hUU/GOMC3hXsTCI6d79y4XX+0HbnD7FT5khegA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uk": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "2uEfujwXKNm6bdpukaLtEJD+04uUtQD65nSGCetA1fYNizItEaIBUboNfr3GzJxSMQotNwGVM3+nSn8jTd0VSg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uz-Cyrl-UZ": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "TD3ME2sprAvFqk9tkWrvSKx5XxEMlAn1sjk+cYClSWZlIMhQQ2Bp/w0VjX1Kc5oeKjxRAnR7vFcLUFLiZIDk9Q==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uz-Latn-UZ": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "/kHAoF4g0GahnugZiEMpaHlxb+W6jCEbWIdsq9/I1k48ULOsl/J0pxZj93lXC3omGzVF1BTVIeAtv5fW06Phsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.vi": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rsQNh9rmHMBtnsUUlJbShMsIMGflZtPmrMM6JNDw20nhsvqfrdcoDD8cMnLAbuSovtc3dP+swRmLQzKmXDTVPA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-CN": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "uH2dWhrgugkCjDmduLdAFO9w1Mo0q07EuvM0QiIZCVm6FMCu/lGv2fpMu4GX+4HLZ6h5T2Pg9FIdDLCPN2a67w==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-Hans": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "WH6IhJ8V1UBG7rZXQk3dZUoP2gsi8a0WkL8xL0sN6WGiv695s8nVcmab9tWz20ySQbuzp0UkSxUQFi5jJHIpOQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-Hant": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "VIXB7HCUC34OoaGnO3HJVtSv2/wljPhjV7eKH4+TFPgQdJj2lvHNKY41Dtg0Bphu7X5UaXFR4zrYYyo+GNOjbA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
}
},
"net8.0/win-x64": {}
}
}

View File

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

View File

@ -0,0 +1,82 @@
# This represents a basic .NET project build where the project dependencies are downloaded and the project is built.
# The output is a directory tree of DLLs, a project.lock.json (not used in these tests), a .deps.json file, and
# a .runtimeconfig.json file (not used in these tests). With this deployment strategy there is no bundled runtime.
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:8.0-alpine@sha256:7d3a75ca5c8ac4679908ef7a2591b9bc257c62bd530167de32bba105148bb7be AS build
ARG RUNTIME=win-x64
WORKDIR /src
# copy csproj and restore as distinct layers
COPY src/*.csproj .
COPY src/packages.lock.json .
RUN dotnet restore -r $RUNTIME --verbosity normal --locked-mode
# copy and publish app and libraries
COPY src/ .
RUN dotnet publish -r $RUNTIME --no-restore -o /app
# $ dotnet list package --include-transitive
# Project 'dotnetapp' has the following package references
# [net8.0]:
# Top-level Package Requested Resolved
# > Humanizer 2.14.1 2.14.1
# > Newtonsoft.Json 13.0.3 13.0.3
#
# Transitive Package Resolved
# > Humanizer.Core 2.14.1
# > Humanizer.Core.af 2.14.1
# > Humanizer.Core.ar 2.14.1
# > Humanizer.Core.az 2.14.1
# > Humanizer.Core.bg 2.14.1
# > Humanizer.Core.bn-BD 2.14.1
# > Humanizer.Core.cs 2.14.1
# > Humanizer.Core.da 2.14.1
# > Humanizer.Core.de 2.14.1
# > Humanizer.Core.el 2.14.1
# > Humanizer.Core.es 2.14.1
# > Humanizer.Core.fa 2.14.1
# > Humanizer.Core.fi-FI 2.14.1
# > Humanizer.Core.fr 2.14.1
# > Humanizer.Core.fr-BE 2.14.1
# > Humanizer.Core.he 2.14.1
# > Humanizer.Core.hr 2.14.1
# > Humanizer.Core.hu 2.14.1
# > Humanizer.Core.hy 2.14.1
# > Humanizer.Core.id 2.14.1
# > Humanizer.Core.is 2.14.1
# > Humanizer.Core.it 2.14.1
# > Humanizer.Core.ja 2.14.1
# > Humanizer.Core.ko-KR 2.14.1
# > Humanizer.Core.ku 2.14.1
# > Humanizer.Core.lv 2.14.1
# > Humanizer.Core.ms-MY 2.14.1
# > Humanizer.Core.mt 2.14.1
# > Humanizer.Core.nb 2.14.1
# > Humanizer.Core.nb-NO 2.14.1
# > Humanizer.Core.nl 2.14.1
# > Humanizer.Core.pl 2.14.1
# > Humanizer.Core.pt 2.14.1
# > Humanizer.Core.ro 2.14.1
# > Humanizer.Core.ru 2.14.1
# > Humanizer.Core.sk 2.14.1
# > Humanizer.Core.sl 2.14.1
# > Humanizer.Core.sr 2.14.1
# > Humanizer.Core.sr-Latn 2.14.1
# > Humanizer.Core.sv 2.14.1
# > Humanizer.Core.th-TH 2.14.1
# > Humanizer.Core.tr 2.14.1
# > Humanizer.Core.uk 2.14.1
# > Humanizer.Core.uz-Cyrl-UZ 2.14.1
# > Humanizer.Core.uz-Latn-UZ 2.14.1
# > Humanizer.Core.vi 2.14.1
# > Humanizer.Core.zh-CN 2.14.1
# > Humanizer.Core.zh-Hans 2.14.1
# > Humanizer.Core.zh-Hant 2.14.1
# lets pull in a file that is not related at all and in fact is not a .NET binary either (this should be ignored)
RUN wget -O /app/jruby_windows_9_3_15_0.exe https://s3.amazonaws.com/jruby.org/downloads/9.3.15.0/jruby_windows_9_3_15_0.exe
FROM busybox
WORKDIR /app
COPY --from=build /app .
# just a nice to have for later...
#COPY --from=build /src/packages.lock.json .

View File

@ -0,0 +1,31 @@
using System;
using System.Net;
using System.Runtime.InteropServices;
using static System.Console;
WriteLine("Runtime and Environment Information");
// OS and .NET information
WriteLine($"{nameof(RuntimeInformation.OSArchitecture)}: {RuntimeInformation.OSArchitecture}");
WriteLine($"{nameof(RuntimeInformation.OSDescription)}: {RuntimeInformation.OSDescription}");
WriteLine($"{nameof(RuntimeInformation.FrameworkDescription)}: {RuntimeInformation.FrameworkDescription}");
WriteLine();
// Environment information
WriteLine($"{nameof(Environment.UserName)}: {Environment.UserName}");
WriteLine($"HostName: {Dns.GetHostName()}");
WriteLine($"{nameof(Environment.ProcessorCount)}: {Environment.ProcessorCount}");
WriteLine();
// Memory information
WriteLine($"Available Memory (GC): {GetInBestUnit(GC.GetGCMemoryInfo().TotalAvailableMemoryBytes)}");
string GetInBestUnit(long size)
{
const double Mebi = 1024 * 1024;
const double Gibi = Mebi * 1024;
if (size < Mebi) return $"{size} bytes";
if (size < Gibi) return $"{size / Mebi:F} MiB";
return $"{size / Gibi:F} GiB";
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Humanizer" Version="2.14.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,459 @@
{
"version": 1,
"dependencies": {
"net8.0": {
"Humanizer": {
"type": "Direct",
"requested": "[2.14.1, )",
"resolved": "2.14.1",
"contentHash": "/FUTD3cEceAAmJSCPN9+J+VhGwmL/C12jvwlyM1DFXShEMsBzvLzLqSrJ2rb+k/W2znKw7JyflZgZpyE+tI7lA==",
"dependencies": {
"Humanizer.Core.af": "2.14.1",
"Humanizer.Core.ar": "2.14.1",
"Humanizer.Core.az": "2.14.1",
"Humanizer.Core.bg": "2.14.1",
"Humanizer.Core.bn-BD": "2.14.1",
"Humanizer.Core.cs": "2.14.1",
"Humanizer.Core.da": "2.14.1",
"Humanizer.Core.de": "2.14.1",
"Humanizer.Core.el": "2.14.1",
"Humanizer.Core.es": "2.14.1",
"Humanizer.Core.fa": "2.14.1",
"Humanizer.Core.fi-FI": "2.14.1",
"Humanizer.Core.fr": "2.14.1",
"Humanizer.Core.fr-BE": "2.14.1",
"Humanizer.Core.he": "2.14.1",
"Humanizer.Core.hr": "2.14.1",
"Humanizer.Core.hu": "2.14.1",
"Humanizer.Core.hy": "2.14.1",
"Humanizer.Core.id": "2.14.1",
"Humanizer.Core.is": "2.14.1",
"Humanizer.Core.it": "2.14.1",
"Humanizer.Core.ja": "2.14.1",
"Humanizer.Core.ko-KR": "2.14.1",
"Humanizer.Core.ku": "2.14.1",
"Humanizer.Core.lv": "2.14.1",
"Humanizer.Core.ms-MY": "2.14.1",
"Humanizer.Core.mt": "2.14.1",
"Humanizer.Core.nb": "2.14.1",
"Humanizer.Core.nb-NO": "2.14.1",
"Humanizer.Core.nl": "2.14.1",
"Humanizer.Core.pl": "2.14.1",
"Humanizer.Core.pt": "2.14.1",
"Humanizer.Core.ro": "2.14.1",
"Humanizer.Core.ru": "2.14.1",
"Humanizer.Core.sk": "2.14.1",
"Humanizer.Core.sl": "2.14.1",
"Humanizer.Core.sr": "2.14.1",
"Humanizer.Core.sr-Latn": "2.14.1",
"Humanizer.Core.sv": "2.14.1",
"Humanizer.Core.th-TH": "2.14.1",
"Humanizer.Core.tr": "2.14.1",
"Humanizer.Core.uk": "2.14.1",
"Humanizer.Core.uz-Cyrl-UZ": "2.14.1",
"Humanizer.Core.uz-Latn-UZ": "2.14.1",
"Humanizer.Core.vi": "2.14.1",
"Humanizer.Core.zh-CN": "2.14.1",
"Humanizer.Core.zh-Hans": "2.14.1",
"Humanizer.Core.zh-Hant": "2.14.1"
}
},
"Newtonsoft.Json": {
"type": "Direct",
"requested": "[13.0.3, )",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"Humanizer.Core": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
},
"Humanizer.Core.af": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "BoQHyu5le+xxKOw+/AUM7CLXneM/Bh3++0qh1u0+D95n6f9eGt9kNc8LcAHLIOwId7Sd5hiAaaav0Nimj3peNw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ar": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "3d1V10LDtmqg5bZjWkA/EkmGFeSfNBcyCH+TiHcHP+HGQQmRq3eBaLcLnOJbVQVn3Z6Ak8GOte4RX4kVCxQlFA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.az": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "8Z/tp9PdHr/K2Stve2Qs/7uqWPWLUK9D8sOZDNzyv42e20bSoJkHFn7SFoxhmaoVLJwku2jp6P7HuwrfkrP18Q==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.bg": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "S+hIEHicrOcbV2TBtyoPp1AVIGsBzlarOGThhQYCnP6QzEYo/5imtok6LMmhZeTnBFoKhM8yJqRfvJ5yqVQKSQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.bn-BD": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "U3bfj90tnUDRKlL1ZFlzhCHoVgpTcqUlTQxjvGCaFKb+734TTu3nkHUWVZltA1E/swTvimo/aXLtkxnLFrc0EQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.cs": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "jWrQkiCTy3L2u1T86cFkgijX6k7hoB0pdcFMWYaSZnm6rvG/XJE40tfhYyKhYYgIc1x9P2GO5AC7xXvFnFdqMQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.da": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "5o0rJyE/2wWUUphC79rgYDnif/21MKTTx9LIzRVz9cjCIVFrJ2bDyR2gapvI9D6fjoyvD1NAfkN18SHBsO8S9g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.de": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "9JD/p+rqjb8f5RdZ3aEJqbjMYkbk4VFii2QDnnOdNo6ywEfg/A5YeOQ55CaBJmy7KvV4tOK4+qHJnX/tg3Z54A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.el": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Xmv6sTL5mqjOWGGpqY7bvbfK5RngaUHSa8fYDGSLyxY9mGdNbDcasnRnMOvi0SxJS9gAqBCn21Xi90n2SHZbFA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.es": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "e//OIAeMB7pjBV1HqqI4pM2Bcw3Jwgpyz9G5Fi4c+RJvhqFwztoWxW57PzTnNJE2lbhGGLQZihFZjsbTUsbczA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fa": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "nzDOj1x0NgjXMjsQxrET21t1FbdoRYujzbmZoR8u8ou5CBWY1UNca0j6n/PEJR/iUbt4IxstpszRy41wL/BrpA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fi-FI": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Vnxxx4LUhp3AzowYi6lZLAA9Lh8UqkdwRh4IE2qDXiVpbo08rSbokATaEzFS+o+/jCNZBmoyyyph3vgmcSzhhQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "2p4g0BYNzFS3u9SOIDByp2VClYKO0K1ecDV4BkB9EYdEPWfFODYnF+8CH8LpUrpxL2TuWo2fiFx/4Jcmrnkbpg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fr-BE": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "o6R3SerxCRn5Ij8nCihDNMGXlaJ/1AqefteAssgmU2qXYlSAGdhxmnrQAXZUDlE4YWt/XQ6VkNLtH7oMqsSPFQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.he": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "FPsAhy7Iw6hb+ZitLgYC26xNcgGAHXb0V823yFAzcyoL5ozM+DCJtYfDPYiOpsJhEZmKFTM9No0jUn1M89WGvg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "chnaD89yOlST142AMkAKLuzRcV5df3yyhDyRU5rypDiqrq2HN8y1UR3h1IicEAEtXLoOEQyjSAkAQ6QuXkn7aw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hu": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "hAfnaoF9LTGU/CmFdbnvugN4tIs8ppevVMe3e5bD24+tuKsggMc5hYta9aiydI8JH9JnuVmxvNI4DJee1tK05A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hy": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "sVIKxOiSBUb4gStRHo9XwwAg9w7TNvAXbjy176gyTtaTiZkcjr9aCPziUlYAF07oNz6SdwdC2mwJBGgvZ0Sl2g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.id": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "4Zl3GTvk3a49Ia/WDNQ97eCupjjQRs2iCIZEQdmkiqyaLWttfb+cYXDMGthP42nufUL0SRsvBctN67oSpnXtsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.is": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "R67A9j/nNgcWzU7gZy1AJ07ABSLvogRbqOWvfRDn4q6hNdbg/mjGjZBp4qCTPnB2mHQQTCKo3oeCUayBCNIBCw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.it": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "jYxGeN4XIKHVND02FZ+Woir3CUTyBhLsqxu9iqR/9BISArkMf1Px6i5pRZnvq4fc5Zn1qw71GKKoCaHDJBsLFw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ja": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "TM3ablFNoYx4cYJybmRgpDioHpiKSD7q0QtMrmpsqwtiiEsdW5zz/q4PolwAczFnvrKpN6nBXdjnPPKVet93ng==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ko-KR": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "CtvwvK941k/U0r8PGdEuBEMdW6jv/rBiA9tUhakC7Zd2rA/HCnDcbr1DiNZ+/tRshnhzxy/qwmpY8h4qcAYCtQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ku": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "vHmzXcVMe+LNrF9txpdHzpG7XJX65SiN9GQd/Zkt6gsGIIEeECHrkwCN5Jnlkddw2M/b0HS4SNxdR1GrSn7uCA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.lv": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "E1/KUVnYBS1bdOTMNDD7LV/jdoZv/fbWTLPtvwdMtSdqLyRTllv6PGM9xVQoFDYlpvVGtEl/09glCojPHw8ffA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ms-MY": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "vX8oq9HnYmAF7bek4aGgGFJficHDRTLgp/EOiPv9mBZq0i4SA96qVMYSjJ2YTaxs7Eljqit7pfpE2nmBhY5Fnw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.mt": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "pEgTBzUI9hzemF7xrIZigl44LidTUhNu4x/P6M9sAwZjkUF0mMkbpxKkaasOql7lLafKrnszs0xFfaxQyzeuZQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nb": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "mbs3m6JJq53ssLqVPxNfqSdTxAcZN3njlG8yhJVx83XVedpTe1ECK9aCa8FKVOXv93Gl+yRHF82Hw9T9LWv2hw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nb-NO": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "AsJxrrVYmIMbKDGe8W6Z6//wKv9dhWH7RsTcEHSr4tQt/80pcNvLi0hgD3fqfTtg0tWKtgch2cLf4prorEV+5A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "24b0OUdzJxfoqiHPCtYnR5Y4l/s4Oh7KW7uDp+qX25NMAHLCGog2eRfA7p2kRJp8LvnynwwQxm2p534V9m55wQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.pl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "17mJNYaBssENVZyQHduiq+bvdXS0nhZJGEXtPKoMhKv3GD//WO0mEfd9wjEBsWCSmWI7bjRqhCidxzN+YtJmsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.pt": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "8HB8qavcVp2la1GJX6t+G9nDYtylPKzyhxr9LAooIei9MnQvNsjEiIE4QvHoeDZ4weuQ9CsPg1c211XUMVEZ4A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ro": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "psXNOcA6R8fSHoQYhpBTtTTYiOk8OBoN3PKCEDgsJKIyeY5xuK81IBdGi77qGZMu/OwBRQjQCBMtPJb0f4O1+A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ru": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "zm245xUWrajSN2t9H7BTf84/2APbUkKlUJpcdgsvTdAysr1ag9fi1APu6JEok39RRBXDfNRVZHawQ/U8X0pSvQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sk": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Ncw24Vf3ioRnbU4MsMFHafkyYi8JOnTqvK741GftlQvAbULBoTz2+e7JByOaasqeSi0KfTXeegJO+5Wk1c0Mbw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "l8sUy4ciAIbVThWNL0atzTS2HWtv8qJrsGWNlqrEKmPwA4SdKolSqnTes9V89fyZTc2Q43jK8fgzVE2C7t009A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rnNvhpkOrWEymy7R/MiFv7uef8YO5HuXDyvojZ7JpijHWA5dXuVXooCOiA/3E93fYa3pxDuG2OQe4M/olXbQ7w==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sr-Latn": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "nuy/ykpk974F8ItoQMS00kJPr2dFNjOSjgzCwfysbu7+gjqHmbLcYs7G4kshLwdA4AsVncxp99LYeJgoh1JF5g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sv": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "E53+tpAG0RCp+cSSI7TfBPC+NnsEqUuoSV0sU+rWRXWr9MbRWx1+Zj02XMojqjGzHjjOrBFBBio6m74seFl0AA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.th-TH": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "eSevlJtvs1r4vQarNPfZ2kKDp/xMhuD00tVVzRXkSh1IAZbBJI/x2ydxUOwfK9bEwEp+YjvL1Djx2+kw7ziu7g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.tr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rQ8N+o7yFcFqdbtu1mmbrXFi8TQ+uy+fVH9OPI0CI3Cu1om5hUU/GOMC3hXsTCI6d79y4XX+0HbnD7FT5khegA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uk": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "2uEfujwXKNm6bdpukaLtEJD+04uUtQD65nSGCetA1fYNizItEaIBUboNfr3GzJxSMQotNwGVM3+nSn8jTd0VSg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uz-Cyrl-UZ": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "TD3ME2sprAvFqk9tkWrvSKx5XxEMlAn1sjk+cYClSWZlIMhQQ2Bp/w0VjX1Kc5oeKjxRAnR7vFcLUFLiZIDk9Q==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uz-Latn-UZ": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "/kHAoF4g0GahnugZiEMpaHlxb+W6jCEbWIdsq9/I1k48ULOsl/J0pxZj93lXC3omGzVF1BTVIeAtv5fW06Phsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.vi": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rsQNh9rmHMBtnsUUlJbShMsIMGflZtPmrMM6JNDw20nhsvqfrdcoDD8cMnLAbuSovtc3dP+swRmLQzKmXDTVPA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-CN": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "uH2dWhrgugkCjDmduLdAFO9w1Mo0q07EuvM0QiIZCVm6FMCu/lGv2fpMu4GX+4HLZ6h5T2Pg9FIdDLCPN2a67w==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-Hans": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "WH6IhJ8V1UBG7rZXQk3dZUoP2gsi8a0WkL8xL0sN6WGiv695s8nVcmab9tWz20ySQbuzp0UkSxUQFi5jJHIpOQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-Hant": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "VIXB7HCUC34OoaGnO3HJVtSv2/wljPhjV7eKH4+TFPgQdJj2lvHNKY41Dtg0Bphu7X5UaXFR4zrYYyo+GNOjbA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
}
},
"net8.0/win-x64": {}
}
}

View File

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

View File

@ -0,0 +1,18 @@
# This is the same as the net8-app image, but without the .deps.json file.
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:8.0-alpine@sha256:7d3a75ca5c8ac4679908ef7a2591b9bc257c62bd530167de32bba105148bb7be AS build
ARG RUNTIME=win-x64
WORKDIR /src
# copy csproj and restore as distinct layers
COPY src/*.csproj .
RUN dotnet restore -r $RUNTIME --verbosity normal
# copy and publish app and libraries
COPY src/ .
RUN dotnet publish -r $RUNTIME --no-restore -o /app
FROM busybox
WORKDIR /app
COPY --from=build /app .
# just a nice to have for later...
#COPY --from=build /src/packages.lock.json .

View File

@ -0,0 +1,3 @@
/bin
/obj
/app

View File

@ -0,0 +1,48 @@
using System;
using Humanizer;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace IndirectDependencyExample
{
class Program
{
static void Main(string[] args)
{
string runtimeInfo = "hello world!\n";
Console.WriteLine(runtimeInfo);
Console.WriteLine($"\"this_is_a_test\" to title case: {"this_is_a_test".Humanize(LetterCasing.Title)}");
const string jsonString = @"
{
""message"": ""Hello from JSON!"",
""details"": {
""timestamp"": ""2025-03-26T12:00:00Z"",
""version"": ""1.0.0"",
""metadata"": {
""author"": ""Claude"",
""environment"": ""Development""
}
},
""items"": [
{
""id"": 1,
""name"": ""Item One""
},
{
""id"": 2,
""name"": ""Item Two""
}
]
}";
JObject jsonObject = JObject.Parse(jsonString);
string message = (string)jsonObject["message"];
Console.WriteLine($"Message: {message}");
}
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ILRepackTargetConfigurations>Debug;Release</ILRepackTargetConfigurations>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" PrivateAssets="all" />
<PackageReference Include="Humanizer" Version="2.14.1" PrivateAssets="all" />
<PackageReference Include="ILRepack.FullAuto" Version="1.6.0" PrivateAssets="all" />
</ItemGroup>
</Project>

View File

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

View File

@ -0,0 +1,18 @@
# This is the same as the net8-app image, but without the .deps.json file.
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:8.0-alpine@sha256:7d3a75ca5c8ac4679908ef7a2591b9bc257c62bd530167de32bba105148bb7be AS build
ARG RUNTIME=win-x64
WORKDIR /src
# copy csproj and restore as distinct layers
COPY src/*.csproj .
RUN dotnet restore -r $RUNTIME --verbosity normal
# copy and publish app and libraries
COPY src/ .
RUN dotnet publish -r $RUNTIME --no-restore -o /app
FROM busybox
WORKDIR /app
COPY --from=build /app .
# just a nice to have for later...
#COPY --from=build /src/packages.lock.json .

View File

@ -0,0 +1,3 @@
/bin
/obj
/app

View File

@ -0,0 +1,48 @@
using System;
using Humanizer;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace IndirectDependencyExample
{
class Program
{
static void Main(string[] args)
{
string runtimeInfo = "hello world!\n";
Console.WriteLine(runtimeInfo);
Console.WriteLine($"\"this_is_a_test\" to title case: {"this_is_a_test".Humanize(LetterCasing.Title)}");
const string jsonString = @"
{
""message"": ""Hello from JSON!"",
""details"": {
""timestamp"": ""2025-03-26T12:00:00Z"",
""version"": ""1.0.0"",
""metadata"": {
""author"": ""Claude"",
""environment"": ""Development""
}
},
""items"": [
{
""id"": 1,
""name"": ""Item One""
},
{
""id"": 2,
""name"": ""Item Two""
}
]
}";
JObject jsonObject = JObject.Parse(jsonString);
string message = (string)jsonObject["message"];
Console.WriteLine($"Message: {message}");
}
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" PrivateAssets="all" />
<PackageReference Include="Humanizer" Version="2.14.1" PrivateAssets="all" />
</ItemGroup>
</Project>

View File

@ -7,6 +7,8 @@ type DotnetDepsEntry struct {
Path string `mapstructure:"path" json:"path"`
Sha512 string `mapstructure:"sha512" json:"sha512"`
HashPath string `mapstructure:"hashPath" json:"hashPath"`
Executables map[string]DotnetPortableExecutableEntry `json:"executables,omitempty"`
}
// DotnetPackagesLockEntry is a struct that represents a single entry found in the "dependencies" section in a .NET packages.lock.json file.

View File

@ -9,7 +9,7 @@ import (
const (
// this is the number of packages that should be found in the image-pkg-coverage fixture image
// when analyzed with the squashed scope.
coverageImageSquashedPackageCount = 30
coverageImageSquashedPackageCount = 42
)
func TestPackagesCmdFlags(t *testing.T) {