diff --git a/syft/formats/common/spdxhelpers/source_info.go b/syft/formats/common/spdxhelpers/source_info.go index 71007150ccca..8aeb5b356aa5 100644 --- a/syft/formats/common/spdxhelpers/source_info.go +++ b/syft/formats/common/spdxhelpers/source_info.go @@ -52,6 +52,8 @@ func SourceInfo(p pkg.Package) string { answer = "acquired package info from linux kernel module files" case pkg.NixPkg: answer = "acquired package info from nix store path" + case pkg.Rpkg: + answer = "acquired package info from R-package DESCRIPTION file" default: answer = "acquired package info from the following paths" } diff --git a/syft/formats/common/spdxhelpers/source_info_test.go b/syft/formats/common/spdxhelpers/source_info_test.go index bb4eee7e89dd..a56efff93385 100644 --- a/syft/formats/common/spdxhelpers/source_info_test.go +++ b/syft/formats/common/spdxhelpers/source_info_test.go @@ -223,6 +223,14 @@ func Test_SourceInfo(t *testing.T) { "from nix store path", }, }, + { + input: pkg.Package{ + Type: pkg.Rpkg, + }, + expected: []string{ + "acquired package info from R-package DESCRIPTION file", + }, + }, } var pkgTypes []pkg.Type for _, test := range tests { diff --git a/syft/pkg/cataloger/cataloger.go b/syft/pkg/cataloger/cataloger.go index 1c25e48ac64b..78a995843ad0 100644 --- a/syft/pkg/cataloger/cataloger.go +++ b/syft/pkg/cataloger/cataloger.go @@ -28,6 +28,7 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/php" "github.com/anchore/syft/syft/pkg/cataloger/portage" "github.com/anchore/syft/syft/pkg/cataloger/python" + "github.com/anchore/syft/syft/pkg/cataloger/r" "github.com/anchore/syft/syft/pkg/cataloger/rpm" "github.com/anchore/syft/syft/pkg/cataloger/ruby" "github.com/anchore/syft/syft/pkg/cataloger/rust" @@ -53,6 +54,7 @@ func ImageCatalogers(cfg Config) []pkg.Cataloger { php.NewComposerInstalledCataloger(), portage.NewPortageCataloger(), python.NewPythonPackageCataloger(), + r.NewPackageCataloger(), rpm.NewRpmDBCataloger(), ruby.NewGemSpecCataloger(), sbom.NewSBOMCataloger(), @@ -121,6 +123,7 @@ func AllCatalogers(cfg Config) []pkg.Cataloger { portage.NewPortageCataloger(), python.NewPythonIndexCataloger(), python.NewPythonPackageCataloger(), + r.NewPackageCataloger(), rpm.NewFileCataloger(), rpm.NewRpmDBCataloger(), ruby.NewGemFileLockCataloger(), diff --git a/syft/pkg/cataloger/r/cataloger.go b/syft/pkg/cataloger/r/cataloger.go new file mode 100644 index 000000000000..8cb4774a4af7 --- /dev/null +++ b/syft/pkg/cataloger/r/cataloger.go @@ -0,0 +1,13 @@ +package r + +import ( + "github.com/anchore/syft/syft/pkg/cataloger/generic" +) + +const catalogerName = "r-package-cataloger" + +// NewPackageCataloger returns a new R cataloger object based on detection of R package DESCRIPTION files. +func NewPackageCataloger() *generic.Cataloger { + return generic.NewCataloger(catalogerName). + WithParserByGlobs(parseDescriptionFile, "**/DESCRIPTION") +} diff --git a/syft/pkg/cataloger/r/cataloger_test.go b/syft/pkg/cataloger/r/cataloger_test.go new file mode 100644 index 000000000000..c1744804d913 --- /dev/null +++ b/syft/pkg/cataloger/r/cataloger_test.go @@ -0,0 +1,60 @@ +package r + +import ( + "testing" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" + "github.com/anchore/syft/syft/source" +) + +func TestRPackageCataloger(t *testing.T) { + expectedPkgs := []pkg.Package{ + { + Name: "base", + Version: "4.3.0", + FoundBy: "r-package-cataloger", + Locations: source.NewLocationSet(source.NewLocation("base/DESCRIPTION")), + Licenses: []string{"Part of R 4.3.0"}, + Language: "R", + Type: "R-package", + PURL: "pkg:cran/base@4.3.0", + MetadataType: "RDescriptionFileMetadataType", + Metadata: pkg.RDescriptionFileMetadata{ + Title: "The R Base Package", + Description: "Base R functions.", + Author: "R Core Team and contributors worldwide", + Maintainer: "R Core Team ", + Built: "R 4.3.0; ; 2023-04-21 11:33:09 UTC; unix", + Suggests: []string{"methods"}, + }, + }, + { + Name: "stringr", + Version: "1.5.0.9000", + FoundBy: "r-package-cataloger", + Locations: source.NewLocationSet(source.NewLocation("stringr/DESCRIPTION")), + Licenses: []string{"MIT + file LICENSE"}, + Language: "R", + Type: "R-package", + PURL: "pkg:cran/stringr@1.5.0.9000", + MetadataType: "RDescriptionFileMetadataType", + Metadata: pkg.RDescriptionFileMetadata{ + Title: "Simple, Consistent Wrappers for Common String Operations", + Description: "A consistent, simple and easy to use set of wrappers around the fantastic 'stringi' package. All function and argument names (and positions) are consistent, all functions deal with \"NA\"'s and zero length vectors in the same way, and the output from one function is easy to feed into the input of another.", + URL: []string{"https://stringr.tidyverse.org", "https://github.com/tidyverse/stringr"}, + Imports: []string{ + "cli", "glue (>= 1.6.1)", "lifecycle (>= 1.0.3)", "magrittr", + "rlang (>= 1.0.0)", "stringi (>= 1.5.3)", "vctrs (>= 0.4.0)", + }, + Depends: []string{"R (>= 3.3)"}, + Suggests: []string{"covr", "dplyr", "gt", "htmltools", "htmlwidgets", "knitr", "rmarkdown", "testthat (>= 3.0.0)", "tibble"}, + }, + }, + } + // TODO: relationships are not under test yet + var expectedRelationships []artifact.Relationship + + pkgtest.NewCatalogTester().FromDirectory(t, "test-fixtures/installed").Expects(expectedPkgs, expectedRelationships).TestCataloger(t, NewPackageCataloger()) +} diff --git a/syft/pkg/cataloger/r/package.go b/syft/pkg/cataloger/r/package.go new file mode 100644 index 000000000000..891088ba0ca4 --- /dev/null +++ b/syft/pkg/cataloger/r/package.go @@ -0,0 +1,38 @@ +package r + +import ( + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +func newPackage(pd parseData, locations ...source.Location) pkg.Package { + locationSet := source.NewLocationSet() + for _, loc := range locations { + locationSet.Add(loc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) + } + result := pkg.Package{ + Name: pd.Package, + Version: pd.Version, + FoundBy: catalogerName, + Locations: locationSet, + Licenses: []string{pd.License}, + Language: "R", + Type: pkg.Rpkg, + PURL: packageURL(pd), + MetadataType: pkg.RDescriptionFileMetadataType, + Metadata: pd.RDescriptionFileMetadata, + } + + result.Language = "R" + result.FoundBy = catalogerName + + result.Licenses = []string{pd.License} + result.Version = pd.Version + result.SetID() + return result +} + +func packageURL(m parseData) string { + return packageurl.NewPackageURL("cran", "", m.Package, m.Version, nil, "").ToString() +} diff --git a/syft/pkg/cataloger/r/parse_description.go b/syft/pkg/cataloger/r/parse_description.go new file mode 100644 index 000000000000..c0e5f762746c --- /dev/null +++ b/syft/pkg/cataloger/r/parse_description.go @@ -0,0 +1,134 @@ +package r + +import ( + "bufio" + "io" + "regexp" + "strings" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" + "github.com/anchore/syft/syft/source" +) + +/* some examples of license strings found in DESCRIPTION files: +find /usr/local/lib/R -name DESCRIPTION | xargs cat | grep 'License:' | sort | uniq +License: GPL +License: GPL (>= 2) +License: GPL (>=2) +License: GPL(>=2) +License: GPL (>= 2) | file LICENCE +License: GPL-2 | GPL-3 +License: GPL-3 +License: LGPL (>= 2) +License: LGPL (>= 2.1) +License: MIT + file LICENSE +License: Part of R 4.3.0 +License: Unlimited +*/ + +func parseDescriptionFile(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + values := extractFieldsFromDescriptionFile(reader) + m := parseDataFromDescriptionMap(values) + return []pkg.Package{newPackage(m, []source.Location{reader.Location}...)}, nil, nil +} + +type parseData struct { + Package string + Version string + License string + pkg.RDescriptionFileMetadata +} + +func parseDataFromDescriptionMap(values map[string]string) parseData { + return parseData{ + License: values["License"], + Package: values["Package"], + Version: values["Version"], + RDescriptionFileMetadata: pkg.RDescriptionFileMetadata{ + Title: values["Title"], + Description: cleanMultiLineValue(values["Description"]), + Maintainer: values["Maintainer"], + URL: commaSeparatedList(values["URL"]), + Depends: commaSeparatedList(values["Depends"]), + Imports: commaSeparatedList(values["Imports"]), + Suggests: commaSeparatedList(values["Suggests"]), + NeedsCompilation: yesNoToBool(values["NeedsCompilation"]), + Author: values["Author"], + Repository: values["Repository"], + Built: values["Built"], + }, + } +} + +func yesNoToBool(s string) bool { + return strings.EqualFold(s, "yes") +} + +func commaSeparatedList(s string) []string { + var result []string + split := strings.Split(s, ",") + for _, piece := range split { + value := strings.TrimSpace(piece) + if value == "" { + continue + } + result = append(result, value) + } + return result +} + +var space = regexp.MustCompile(`\s+`) + +func cleanMultiLineValue(s string) string { + return space.ReplaceAllString(s, " ") +} + +func extractFieldsFromDescriptionFile(reader io.Reader) map[string]string { + result := make(map[string]string) + key := "" + var valueFragment strings.Builder + scanner := bufio.NewScanner(reader) + + for scanner.Scan() { + line := scanner.Text() + // line is like Key: Value -> start capturing value; close out previous value + // line is like \t\t continued value -> append to existing value + if len(line) == 0 { + continue + } + if startsWithWhitespace(line) { + // we're continuing a value + if key == "" { + continue + } + valueFragment.WriteByte('\n') + valueFragment.WriteString(strings.TrimSpace(line)) + } else { + if key != "" { + // capture previous value + result[key] = valueFragment.String() + key = "" + valueFragment = strings.Builder{} + } + parts := strings.SplitN(line, ":", 2) + if len(parts) != 2 { + continue + } + key = parts[0] + valueFragment.WriteString(strings.TrimSpace(parts[1])) + } + } + if key != "" { + result[key] = valueFragment.String() + } + return result +} + +func startsWithWhitespace(s string) bool { + if s == "" { + return false + } + return s[0] == ' ' || s[0] == '\t' +} diff --git a/syft/pkg/cataloger/r/parse_description_test.go b/syft/pkg/cataloger/r/parse_description_test.go new file mode 100644 index 000000000000..82f70df6a3ac --- /dev/null +++ b/syft/pkg/cataloger/r/parse_description_test.go @@ -0,0 +1,73 @@ +package r + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_extractFieldsFromDescriptionFile(t *testing.T) { + tests := []struct { + name string + fixture string + want map[string]string + }{ + { + name: "go case", + fixture: "test-fixtures/map-parse/simple", + want: map[string]string{ + "Package": "base", + "Version": "4.3.0", + "Suggests": "methods", + "Built": "R 4.3.0; ; 2023-04-21 11:33:09 UTC; unix", + }, + }, + { + name: "bad cases", + fixture: "test-fixtures/map-parse/bad", + want: map[string]string{ + "Key": "", + "Whitespace": "", + }, + }, + { + name: "multiline key-value", + fixture: "test-fixtures/map-parse/multiline", + want: map[string]string{ + "Description": `A consistent, simple and easy to use set of wrappers around +the fantastic 'stringi' package. All function and argument names (and +positions) are consistent, all functions deal with "NA"'s and zero +length vectors in the same way, and the output from one function is +easy to feed into the input of another.`, + "License": "MIT + file LICENSE", + "Key": "value", + }, + }, + { + name: "eof multiline", + fixture: "test-fixtures/map-parse/eof-multiline", + want: map[string]string{ + "License": "MIT + file LICENSE", + "Description": `A consistent, simple and easy to use set of wrappers around +the fantastic 'stringi' package. All function and argument names (and +positions) are consistent, all functions deal with "NA"'s and zero +length vectors in the same way, and the output from one function is +easy to feed into the input of another.`, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + file, err := os.Open(test.fixture) + require.NoError(t, err) + + result := extractFieldsFromDescriptionFile(file) + + assert.Equal(t, test.want, result) + }) + } + +} diff --git a/syft/pkg/cataloger/r/test-fixtures/DESCRIPTION b/syft/pkg/cataloger/r/test-fixtures/DESCRIPTION new file mode 100644 index 000000000000..ccc610298943 --- /dev/null +++ b/syft/pkg/cataloger/r/test-fixtures/DESCRIPTION @@ -0,0 +1,46 @@ +Package: stringr +Title: Simple, Consistent Wrappers for Common String Operations +Version: 1.5.0.9000 +Authors@R: + c(person(given = "Hadley", + family = "Wickham", + role = c("aut", "cre", "cph"), + email = "hadley@rstudio.com"), + person(given = "RStudio", + role = c("cph", "fnd"))) +Description: A consistent, simple and easy to use set of wrappers around + the fantastic 'stringi' package. All function and argument names (and + positions) are consistent, all functions deal with "NA"'s and zero + length vectors in the same way, and the output from one function is + easy to feed into the input of another. +License: MIT + file LICENSE +URL: https://stringr.tidyverse.org, https://github.com/tidyverse/stringr +BugReports: https://github.com/tidyverse/stringr/issues +Depends: + R (>= 3.3) +Imports: + cli, + glue (>= 1.6.1), + lifecycle (>= 1.0.3), + magrittr, + rlang (>= 1.0.0), + stringi (>= 1.5.3), + vctrs (>= 0.4.0) +Suggests: + covr, + dplyr, + gt, + htmltools, + htmlwidgets, + knitr, + rmarkdown, + testthat (>= 3.0.0), + tibble +VignetteBuilder: + knitr +Config/Needs/website: tidyverse/tidytemplate +Config/testthat/edition: 3 +Encoding: UTF-8 +LazyData: true +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.2.1 diff --git a/syft/pkg/cataloger/r/test-fixtures/installed/base/DESCRIPTION b/syft/pkg/cataloger/r/test-fixtures/installed/base/DESCRIPTION new file mode 100644 index 000000000000..f47edbdd0db0 --- /dev/null +++ b/syft/pkg/cataloger/r/test-fixtures/installed/base/DESCRIPTION @@ -0,0 +1,11 @@ +Package: base +Version: 4.3.0 +Priority: base +Title: The R Base Package +Author: R Core Team and contributors worldwide +Maintainer: R Core Team +Contact: R-help mailing list +Description: Base R functions. +License: Part of R 4.3.0 +Suggests: methods +Built: R 4.3.0; ; 2023-04-21 11:33:09 UTC; unix \ No newline at end of file diff --git a/syft/pkg/cataloger/r/test-fixtures/installed/stringr/DESCRIPTION b/syft/pkg/cataloger/r/test-fixtures/installed/stringr/DESCRIPTION new file mode 100644 index 000000000000..ccc610298943 --- /dev/null +++ b/syft/pkg/cataloger/r/test-fixtures/installed/stringr/DESCRIPTION @@ -0,0 +1,46 @@ +Package: stringr +Title: Simple, Consistent Wrappers for Common String Operations +Version: 1.5.0.9000 +Authors@R: + c(person(given = "Hadley", + family = "Wickham", + role = c("aut", "cre", "cph"), + email = "hadley@rstudio.com"), + person(given = "RStudio", + role = c("cph", "fnd"))) +Description: A consistent, simple and easy to use set of wrappers around + the fantastic 'stringi' package. All function and argument names (and + positions) are consistent, all functions deal with "NA"'s and zero + length vectors in the same way, and the output from one function is + easy to feed into the input of another. +License: MIT + file LICENSE +URL: https://stringr.tidyverse.org, https://github.com/tidyverse/stringr +BugReports: https://github.com/tidyverse/stringr/issues +Depends: + R (>= 3.3) +Imports: + cli, + glue (>= 1.6.1), + lifecycle (>= 1.0.3), + magrittr, + rlang (>= 1.0.0), + stringi (>= 1.5.3), + vctrs (>= 0.4.0) +Suggests: + covr, + dplyr, + gt, + htmltools, + htmlwidgets, + knitr, + rmarkdown, + testthat (>= 3.0.0), + tibble +VignetteBuilder: + knitr +Config/Needs/website: tidyverse/tidytemplate +Config/testthat/edition: 3 +Encoding: UTF-8 +LazyData: true +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.2.1 diff --git a/syft/pkg/cataloger/r/test-fixtures/map-parse/bad b/syft/pkg/cataloger/r/test-fixtures/map-parse/bad new file mode 100644 index 000000000000..a2ea102351b0 --- /dev/null +++ b/syft/pkg/cataloger/r/test-fixtures/map-parse/bad @@ -0,0 +1,3 @@ +MissingColon +Whitespace: +Key: \ No newline at end of file diff --git a/syft/pkg/cataloger/r/test-fixtures/map-parse/eof-multiline b/syft/pkg/cataloger/r/test-fixtures/map-parse/eof-multiline new file mode 100644 index 000000000000..ecc14bf7d61d --- /dev/null +++ b/syft/pkg/cataloger/r/test-fixtures/map-parse/eof-multiline @@ -0,0 +1,6 @@ +License: MIT + file LICENSE +Description: A consistent, simple and easy to use set of wrappers around + the fantastic 'stringi' package. All function and argument names (and + positions) are consistent, all functions deal with "NA"'s and zero + length vectors in the same way, and the output from one function is + easy to feed into the input of another. \ No newline at end of file diff --git a/syft/pkg/cataloger/r/test-fixtures/map-parse/multiline b/syft/pkg/cataloger/r/test-fixtures/map-parse/multiline new file mode 100644 index 000000000000..35e019c43e23 --- /dev/null +++ b/syft/pkg/cataloger/r/test-fixtures/map-parse/multiline @@ -0,0 +1,8 @@ +Key: value + +Description: A consistent, simple and easy to use set of wrappers around + the fantastic 'stringi' package. All function and argument names (and + positions) are consistent, all functions deal with "NA"'s and zero + length vectors in the same way, and the output from one function is + easy to feed into the input of another. +License: MIT + file LICENSE \ No newline at end of file diff --git a/syft/pkg/cataloger/r/test-fixtures/map-parse/simple b/syft/pkg/cataloger/r/test-fixtures/map-parse/simple new file mode 100644 index 000000000000..74bb84692a28 --- /dev/null +++ b/syft/pkg/cataloger/r/test-fixtures/map-parse/simple @@ -0,0 +1,4 @@ +Package: base +Version: 4.3.0 +Suggests: methods +Built: R 4.3.0; ; 2023-04-21 11:33:09 UTC; unix diff --git a/syft/pkg/language.go b/syft/pkg/language.go index 2ce12163e4ed..bb2902bbf605 100644 --- a/syft/pkg/language.go +++ b/syft/pkg/language.go @@ -23,6 +23,7 @@ const ( JavaScript Language = "javascript" PHP Language = "php" Python Language = "python" + R Language = "R" Ruby Language = "ruby" Rust Language = "rust" Swift Language = "swift" @@ -41,6 +42,7 @@ var AllLanguages = []Language{ JavaScript, PHP, Python, + R, Ruby, Rust, Swift, @@ -91,6 +93,8 @@ func LanguageByName(name string) Language { // answer: no. We want this to definitively answer "which language does this package represent?" // which might not be possible in all cases. See for more context: https://github.com/package-url/purl-spec/pull/178 return UnknownLanguage + case packageurl.TypeCran, "r": + return R default: return UnknownLanguage } diff --git a/syft/pkg/language_test.go b/syft/pkg/language_test.go index 8e84b975eb1e..36740d66ca4a 100644 --- a/syft/pkg/language_test.go +++ b/syft/pkg/language_test.go @@ -66,6 +66,10 @@ func TestLanguageFromPURL(t *testing.T) { purl: "pkg:hex/hpax/hpax@0.1.1", want: UnknownLanguage, }, + { + purl: "pkg:cran/base@4.3.0", + want: R, + }, } var languages []string @@ -231,6 +235,10 @@ func TestLanguageByName(t *testing.T) { name: "haskell", language: Haskell, }, + { + name: "R", + language: R, + }, } for _, test := range tests { diff --git a/syft/pkg/metadata.go b/syft/pkg/metadata.go index c6d4a036ec64..5d43e9911def 100644 --- a/syft/pkg/metadata.go +++ b/syft/pkg/metadata.go @@ -38,6 +38,7 @@ const ( PythonPipfileLockMetadataType MetadataType = "PythonPipfileLockMetadata" PythonRequirementsMetadataType MetadataType = "PythonRequirementsMetadata" RebarLockMetadataType MetadataType = "RebarLockMetadataType" + RDescriptionFileMetadataType MetadataType = "RDescriptionFileMetadataType" RpmMetadataType MetadataType = "RpmMetadata" RustCargoPackageMetadataType MetadataType = "RustCargoPackageMetadata" ) @@ -69,6 +70,7 @@ var AllMetadataTypes = []MetadataType{ PythonPackageMetadataType, PythonPipfileLockMetadataType, PythonRequirementsMetadataType, + RDescriptionFileMetadataType, RebarLockMetadataType, RpmMetadataType, RustCargoPackageMetadataType, @@ -101,6 +103,7 @@ var MetadataTypeByName = map[MetadataType]reflect.Type{ PythonPackageMetadataType: reflect.TypeOf(PythonPackageMetadata{}), PythonPipfileLockMetadataType: reflect.TypeOf(PythonPipfileLockMetadata{}), PythonRequirementsMetadataType: reflect.TypeOf(PythonRequirementsMetadata{}), + RDescriptionFileMetadataType: reflect.TypeOf(RDescriptionFileMetadata{}), RebarLockMetadataType: reflect.TypeOf(RebarLockMetadata{}), RpmMetadataType: reflect.TypeOf(RpmMetadata{}), RustCargoPackageMetadataType: reflect.TypeOf(CargoPackageMetadata{}), diff --git a/syft/pkg/r_package_metadata.go b/syft/pkg/r_package_metadata.go new file mode 100644 index 000000000000..2a3ad3204d15 --- /dev/null +++ b/syft/pkg/r_package_metadata.go @@ -0,0 +1,21 @@ +package pkg + +type RDescriptionFileMetadata struct { + /* + Fields chosen by: + docker run --rm -it rocker/r-ver bash + $ install2.r ggplot2 # has a lot of dependencies + $ find /usr/local/lib/R -name DESCRIPTION | xargs cat | grep -v '^\s' | cut -d ':' -f 1 | sort | uniq -c | sort -nr + */ + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Author string `json:"author,omitempty"` + Maintainer string `json:"maintainer,omitempty"` + URL []string `json:"url,omitempty"` + Repository string `json:"repository,omitempty"` + Built string `json:"built,omitempty"` + NeedsCompilation bool `json:"needsCompilation,omitempty"` + Imports []string `json:"imports,omitempty"` + Depends []string `json:"depends,omitempty"` + Suggests []string `json:"suggests,omitempty"` +} diff --git a/syft/pkg/type.go b/syft/pkg/type.go index 952ec2e99300..760b3232931c 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -33,6 +33,7 @@ const ( PhpComposerPkg Type = "php-composer" PortagePkg Type = "portage" PythonPkg Type = "python" + Rpkg Type = "R-package" RpmPkg Type = "rpm" RustPkg Type = "rust-crate" ) @@ -61,6 +62,7 @@ var AllPkgs = []Type{ PhpComposerPkg, PortagePkg, PythonPkg, + Rpkg, RpmPkg, RustPkg, } @@ -106,6 +108,8 @@ func (t Type) PackageURLType() string { return "nix" case NpmPkg: return packageurl.TypeNPM + case Rpkg: + return packageurl.TypeCran case RpmPkg: return packageurl.TypeRPM case RustPkg: @@ -173,6 +177,8 @@ func TypeByName(name string) Type { return LinuxKernelModulePkg case "nix": return NixPkg + case packageurl.TypeCran: + return Rpkg default: return UnknownPkg } diff --git a/syft/pkg/type_test.go b/syft/pkg/type_test.go index cfcd4b1d9ddd..e5c7a687fd8f 100644 --- a/syft/pkg/type_test.go +++ b/syft/pkg/type_test.go @@ -91,6 +91,10 @@ func TestTypeFromPURL(t *testing.T) { purl: "pkg:nix/glibc@2.34?hash=h0cnbmfcn93xm5dg2x27ixhag1cwndga", expected: NixPkg, }, + { + purl: "pkg:cran/base@4.3.0", + expected: Rpkg, + }, } var pkgTypes []string diff --git a/test/integration/catalog_packages_cases_test.go b/test/integration/catalog_packages_cases_test.go index bc3acbdf2fdf..39a7e1c79d26 100644 --- a/test/integration/catalog_packages_cases_test.go +++ b/test/integration/catalog_packages_cases_test.go @@ -69,6 +69,14 @@ var imageOnlyTestCases = []testCase{ "joda-time": "2.9.2", }, }, + { + name: "find R packages", + pkgType: pkg.Rpkg, + pkgLanguage: "R", + pkgInfo: map[string]string{ + "base": "4.3.0", + }, + }, } var dirOnlyTestCases = []testCase{ diff --git a/test/integration/catalog_packages_test.go b/test/integration/catalog_packages_test.go index 8b65ed9a9ba8..c700484cd72e 100644 --- a/test/integration/catalog_packages_test.go +++ b/test/integration/catalog_packages_test.go @@ -163,12 +163,14 @@ func TestPkgCoverageDirectory(t *testing.T) { for _, l := range pkg.AllLanguages { definedLanguages.Add(l.String()) } + definedLanguages.Remove(string(pkg.R)) observedPkgs := internal.NewStringSet() definedPkgs := internal.NewStringSet() for _, p := range pkg.AllPkgs { definedPkgs.Add(string(p)) } + definedPkgs.Remove(string(pkg.Rpkg)) var cases []testCase cases = append(cases, commonTestCases...) diff --git a/test/integration/test-fixtures/image-pkg-coverage/pkgs/r/base/DESCRIPTION b/test/integration/test-fixtures/image-pkg-coverage/pkgs/r/base/DESCRIPTION new file mode 100644 index 000000000000..f47edbdd0db0 --- /dev/null +++ b/test/integration/test-fixtures/image-pkg-coverage/pkgs/r/base/DESCRIPTION @@ -0,0 +1,11 @@ +Package: base +Version: 4.3.0 +Priority: base +Title: The R Base Package +Author: R Core Team and contributors worldwide +Maintainer: R Core Team +Contact: R-help mailing list +Description: Base R functions. +License: Part of R 4.3.0 +Suggests: methods +Built: R 4.3.0; ; 2023-04-21 11:33:09 UTC; unix \ No newline at end of file