feat(python): Include Conda packages in SBOMs (#3379)

Co-authored-by: knqyf263 <knqyf263@gmail.com>
This commit is contained in:
Matthieu Maitre
2023-01-10 06:11:17 -08:00
committed by GitHub
parent fbd8a13d54
commit b88bccae6e
23 changed files with 677 additions and 31 deletions

View File

@@ -4,11 +4,13 @@
## Other language-specific packages
| Language | File | Dependency location[^1] |
|----------|--------------|:-----------------------:|
| Swift | Podfile.lock | - |
| Language | File | Dependency location[^1] |
|----------|-------------------|:-----------------------:|
| Python | conda package[^2] | - |
| Swift | Podfile.lock | - |
[^1]: Use `startline == 1 and endline == 1` for unsupported file types
[^2]: `envs/*/conda-meta/*.json`
[os_packages]: ../vulnerability/detection/os.md
[language_packages]: ../vulnerability/detection/language.md

View File

@@ -28,6 +28,8 @@ func TestFilesystem(t *testing.T) {
helmValuesFile []string
skipFiles []string
skipDirs []string
command string
format string
}
tests := []struct {
name string
@@ -263,6 +265,24 @@ func TestFilesystem(t *testing.T) {
},
golden: "testdata/secrets.json.golden",
},
{
name: "conda generating CycloneDX SBOM",
args: args{
command: "rootfs",
format: "cyclonedx",
input: "testdata/fixtures/fs/conda",
},
golden: "testdata/conda-cyclonedx.json.golden",
},
{
name: "conda generating SPDX SBOM",
args: args{
command: "rootfs",
format: "spdx-json",
input: "testdata/fixtures/fs/conda",
},
golden: "testdata/conda-spdx.json.golden",
},
}
// Set up testing DB
@@ -273,9 +293,24 @@ func TestFilesystem(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
command := "fs"
if tt.args.command != "" {
command = tt.args.command
}
format := "json"
if tt.args.format != "" {
format = tt.args.format
}
osArgs := []string{
"-q", "--cache-dir", cacheDir, "fs", "--skip-db-update", "--skip-policy-update",
"--format", "json", "--offline-scan", "--security-checks", tt.args.securityChecks,
"-q", "--cache-dir", cacheDir, command, "--skip-db-update", "--skip-policy-update",
"--format", format, "--offline-scan",
}
if tt.args.securityChecks != "" {
osArgs = append(osArgs, "--security-checks", tt.args.securityChecks)
}
if len(tt.args.policyPaths) != 0 {
@@ -353,7 +388,16 @@ func TestFilesystem(t *testing.T) {
require.NoError(t, err)
// Compare want and got
compareReports(t, tt.golden, outputFile)
switch format {
case "cyclonedx":
compareCycloneDX(t, tt.golden, outputFile)
case "spdx-json":
compareSpdxJson(t, tt.golden, outputFile)
case "json":
compareReports(t, tt.golden, outputFile)
default:
require.Fail(t, "invalid format", "format: %s", format)
}
})
}
}

View File

@@ -14,6 +14,9 @@ import (
"testing"
"time"
cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/spdx/tools-golang/jsonloader"
"github.com/spdx/tools-golang/spdx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -121,6 +124,50 @@ func readReport(t *testing.T, filePath string) types.Report {
return report
}
func readCycloneDX(t *testing.T, filePath string) *cdx.BOM {
f, err := os.Open(filePath)
require.NoError(t, err)
defer f.Close()
bom := cdx.NewBOM()
decoder := cdx.NewBOMDecoder(f, cdx.BOMFileFormatJSON)
err = decoder.Decode(bom)
require.NoError(t, err)
// We don't compare values which change each time an SBOM is generated
bom.Metadata.Timestamp = ""
bom.Metadata.Component.BOMRef = ""
bom.SerialNumber = ""
if bom.Components != nil {
for i := range *bom.Components {
(*bom.Components)[i].BOMRef = ""
}
}
if bom.Dependencies != nil {
for j := range *bom.Dependencies {
(*bom.Dependencies)[j].Ref = ""
(*bom.Dependencies)[j].Dependencies = nil
}
}
return bom
}
func readSpdxJson(t *testing.T, filePath string) *spdx.Document2_2 {
f, err := os.Open(filePath)
require.NoError(t, err)
defer f.Close()
bom, err := jsonloader.Load2_2(f)
require.NoError(t, err)
// We don't compare values which change each time an SBOM is generated
bom.CreationInfo.Created = ""
bom.CreationInfo.DocumentNamespace = ""
return bom
}
func execute(osArgs []string) error {
// Setup CLI App
app := commands.NewApp("dev")
@@ -136,3 +183,15 @@ func compareReports(t *testing.T, wantFile, gotFile string) {
got := readReport(t, gotFile)
assert.Equal(t, want, got)
}
func compareCycloneDX(t *testing.T, wantFile, gotFile string) {
want := readCycloneDX(t, wantFile)
got := readCycloneDX(t, gotFile)
assert.Equal(t, want, got)
}
func compareSpdxJson(t *testing.T, wantFile, gotFile string) {
want := readSpdxJson(t, wantFile)
got := readSpdxJson(t, gotFile)
assert.Equal(t, want, got)
}

View File

@@ -3,11 +3,9 @@
package integration
import (
"os"
"path/filepath"
"testing"
cdx "github.com/CycloneDX/cyclonedx-go"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -128,9 +126,7 @@ func TestSBOM(t *testing.T) {
// Compare want and got
switch tt.args.format {
case "cyclonedx":
want := decodeCycloneDX(t, tt.golden)
got := decodeCycloneDX(t, outputFile)
assert.Equal(t, want, got)
compareCycloneDX(t, tt.golden, outputFile)
case "json":
compareSBOMReports(t, tt.golden, outputFile, tt.override)
default:
@@ -165,18 +161,3 @@ func compareSBOMReports(t *testing.T, wantFile, gotFile string, overrideWant typ
got := readReport(t, gotFile)
assert.Equal(t, want, got)
}
func decodeCycloneDX(t *testing.T, filePath string) *cdx.BOM {
f, err := os.Open(filePath)
require.NoError(t, err)
defer f.Close()
bom := cdx.NewBOM()
decoder := cdx.NewBOMDecoder(f, cdx.BOMFileFormatJSON)
err = decoder.Decode(bom)
require.NoError(t, err)
bom.Metadata.Timestamp = ""
return bom
}

View File

@@ -0,0 +1,83 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:4dd4cf4a-d4de-4ea0-b75f-ad617f31b5a9",
"version": 1,
"metadata": {
"timestamp": "2023-01-08T23:57:37+00:00",
"tools": [
{
"vendor": "aquasecurity",
"name": "trivy",
"version": "dev"
}
],
"component": {
"bom-ref": "582a7c6f-b30e-4b65-a911-f3f5034aa003",
"type": "application",
"name": "testdata/fixtures/fs/conda",
"properties": [
{
"name": "aquasecurity:trivy:SchemaVersion",
"value": "2"
}
]
}
},
"components": [
{
"bom-ref": "pkg:conda/openssl@1.1.1q?file_path=miniconda3%2Fenvs%2Ftestenv%2Fconda-meta%2Fopenssl-1.1.1q-h7f8727e_0.json",
"type": "library",
"name": "openssl",
"version": "1.1.1q",
"licenses": [
{
"expression": "OpenSSL"
}
],
"purl": "pkg:conda/openssl@1.1.1q",
"properties": [
{
"name": "aquasecurity:trivy:PkgType",
"value": "conda-pkg"
},
{
"name": "aquasecurity:trivy:FilePath",
"value": "miniconda3/envs/testenv/conda-meta/openssl-1.1.1q-h7f8727e_0.json"
}
]
},
{
"bom-ref": "pkg:conda/pip@22.2.2?file_path=miniconda3%2Fenvs%2Ftestenv%2Fconda-meta%2Fpip-22.2.2-py38h06a4308_0.json",
"type": "library",
"name": "pip",
"version": "22.2.2",
"licenses": [
{
"expression": "MIT"
}
],
"purl": "pkg:conda/pip@22.2.2",
"properties": [
{
"name": "aquasecurity:trivy:PkgType",
"value": "conda-pkg"
},
{
"name": "aquasecurity:trivy:FilePath",
"value": "miniconda3/envs/testenv/conda-meta/pip-22.2.2-py38h06a4308_0.json"
}
]
}
],
"dependencies": [
{
"ref": "582a7c6f-b30e-4b65-a911-f3f5034aa003",
"dependsOn": [
"pkg:conda/openssl@1.1.1q?file_path=miniconda3%2Fenvs%2Ftestenv%2Fconda-meta%2Fopenssl-1.1.1q-h7f8727e_0.json",
"pkg:conda/pip@22.2.2?file_path=miniconda3%2Fenvs%2Ftestenv%2Fconda-meta%2Fpip-22.2.2-py38h06a4308_0.json"
]
}
],
"vulnerabilities": []
}

View File

@@ -0,0 +1,101 @@
{
"SPDXID": "SPDXRef-DOCUMENT",
"creationInfo": {
"created": "2023-01-08T23:58:16.700785648Z",
"creators": [
"Tool: trivy",
"Organization: aquasecurity"
]
},
"dataLicense": "CC0-1.0",
"documentDescribes": [
"SPDXRef-Filesystem-6e0ac6a0fab50ab4"
],
"documentNamespace": "http://aquasecurity.github.io/trivy/filesystem/testdata/fixtures/fs/conda-3be0d21e-5711-451e-8b1b-2ac8775a3abb",
"files": [
{
"SPDXID": "SPDXRef-File-600e5e0110a84891",
"fileName": "miniconda3/envs/testenv/conda-meta/openssl-1.1.1q-h7f8727e_0.json"
},
{
"SPDXID": "SPDXRef-File-7eb62e2a3edddc0a",
"fileName": "miniconda3/envs/testenv/conda-meta/pip-22.2.2-py38h06a4308_0.json"
}
],
"name": "testdata/fixtures/fs/conda",
"packages": [
{
"SPDXID": "SPDXRef-Application-ee5ef1aa4ac89125",
"filesAnalyzed": false,
"name": "conda-pkg",
"sourceInfo": "Conda"
},
{
"SPDXID": "SPDXRef-Filesystem-6e0ac6a0fab50ab4",
"attributionTexts": [
"SchemaVersion: 2"
],
"filesAnalyzed": false,
"name": "testdata/fixtures/fs/conda"
},
{
"SPDXID": "SPDXRef-Package-2984084f02572600",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:conda/openssl@1.1.1q",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"hasFiles": [
"SPDXRef-File-600e5e0110a84891"
],
"licenseConcluded": "OpenSSL",
"licenseDeclared": "OpenSSL",
"name": "openssl",
"versionInfo": "1.1.1q"
},
{
"SPDXID": "SPDXRef-Package-ac33eb699b3aa81d",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:conda/pip@22.2.2",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"hasFiles": [
"SPDXRef-File-7eb62e2a3edddc0a"
],
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"name": "pip",
"versionInfo": "22.2.2"
}
],
"relationships": [
{
"relatedSpdxElement": "SPDXRef-Filesystem-6e0ac6a0fab50ab4",
"relationshipType": "DESCRIBES",
"spdxElementId": "SPDXRef-DOCUMENT"
},
{
"relatedSpdxElement": "SPDXRef-Application-ee5ef1aa4ac89125",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-Filesystem-6e0ac6a0fab50ab4"
},
{
"relatedSpdxElement": "SPDXRef-Package-2984084f02572600",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-Application-ee5ef1aa4ac89125"
},
{
"relatedSpdxElement": "SPDXRef-Package-ac33eb699b3aa81d",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-Application-ee5ef1aa4ac89125"
}
],
"spdxVersion": "SPDX-2.2"
}

View File

@@ -0,0 +1,60 @@
{
"build": "h7f8727e_0",
"build_number": 0,
"channel": "https://repo.anaconda.com/pkgs/main/linux-64",
"constrains": [],
"depends": [
"ca-certificates",
"libgcc-ng >=7.5.0"
],
"extracted_package_dir": "/home/mmaitre/miniconda3/pkgs/openssl-1.1.1q-h7f8727e_0",
"features": "",
"files": [
"bin/c_rehash",
"<SNIP>",
"ssl/openssl.cnf.dist"
],
"fn": "openssl-1.1.1q-h7f8727e_0.conda",
"legacy_bz2_md5": "ad51928702694e3f6d25b7d4229c84e6",
"license": "OpenSSL",
"license_family": "Apache",
"link": {
"source": "/home/user/miniconda3/pkgs/openssl-1.1.1q-h7f8727e_0",
"type": 1
},
"md5": "2ac47797afee2ece8d339c18b095b8d8",
"name": "openssl",
"package_tarball_full_path": "/home/user/miniconda3/pkgs/openssl-1.1.1q-h7f8727e_0.conda",
"paths_data": {
"paths": [
{
"_path": "bin/c_rehash",
"file_mode": "text",
"path_type": "hardlink",
"prefix_placeholder": "/opt/conda/conda-bld/openssl_1657551138854/_h_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_place",
"sha256": "04c9f4a5c91e20a24d14a36668b5bebe826d6087fb2337b68e33a4d485f5586f",
"sha256_in_prefix": "fc2a6b708cccb8ba90d20e54a9b07257fce009bf315a36f73d3e542dd8674921",
"size_in_bytes": 6991
},
{
"_path": "<SNIP>"
},
{
"_path": "ssl/openssl.cnf.dist",
"path_type": "hardlink",
"sha256": "f10ba64917b4458fafc1e078c2eb9e6a7602e68fc98c2e9e6df5e1636ae27d6b",
"sha256_in_prefix": "f10ba64917b4458fafc1e078c2eb9e6a7602e68fc98c2e9e6df5e1636ae27d6b",
"size_in_bytes": 10909
}
],
"paths_version": 1
},
"requested_spec": "None",
"sha256": "49804293b87141523b2606836ece8e2aaa5202983698fd91e7c36bdb8c8a8de5",
"size": 2649280,
"subdir": "linux-64",
"timestamp": 1657551292835,
"track_features": "",
"url": "https://repo.anaconda.com/pkgs/main/linux-64/openssl-1.1.1q-h7f8727e_0.conda",
"version": "1.1.1q"
}

View File

@@ -0,0 +1,62 @@
{
"build": "py38h06a4308_0",
"build_number": 0,
"channel": "https://repo.anaconda.com/pkgs/main/linux-64",
"constrains": [],
"depends": [
"python >=3.8,<3.9.0a0",
"setuptools",
"wheel"
],
"extracted_package_dir": "/home/user/miniconda3/pkgs/pip-22.2.2-py38h06a4308_0",
"features": "",
"files": [
"bin/pip",
"<SNIP>",
"lib/python3.8/site-packages/pip/py.typed"
],
"fn": "pip-22.2.2-py38h06a4308_0.conda",
"legacy_bz2_md5": "2ac9f1cfec65a1e4ef00cc0132ecd753",
"legacy_bz2_size": 2849993,
"license": "MIT",
"license_family": "MIT",
"link": {
"source": "/home/user/miniconda3/pkgs/pip-22.2.2-py38h06a4308_0",
"type": 1
},
"md5": "ed3e0331e7c614b3148c9911e1fc15e3",
"name": "pip",
"package_tarball_full_path": "/home/user/miniconda3/pkgs/pip-22.2.2-py38h06a4308_0.conda",
"paths_data": {
"paths": [
{
"_path": "bin/pip",
"file_mode": "text",
"path_type": "hardlink",
"prefix_placeholder": "/opt/conda/conda-bld/pip_1664552683240/_h_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold",
"sha256": "1110c03ca2fb86e43e8b52a61cd9d7c722b2817fc893a613e1a947f5d7049008",
"sha256_in_prefix": "14ed5ba79d096035b83a469e863acbbaadd3ea6ca5f23f79eefa91fa857d3f19",
"size_in_bytes": 475
},
{
"_path": "<SNIP>"
},
{
"_path": "lib/python3.8/site-packages/pip/py.typed",
"path_type": "hardlink",
"sha256": "10156fbcf4539ff788a73e5ee50ced48276b317ed0c1ded53fddd14a82256762",
"sha256_in_prefix": "10156fbcf4539ff788a73e5ee50ced48276b317ed0c1ded53fddd14a82256762",
"size_in_bytes": 286
}
],
"paths_version": 1
},
"requested_spec": "None",
"sha256": "3fb76b94cfa5ea9732bfb241b3d234ec0a5a48d16755c3c1ef3c94630f91eb26",
"size": 2417732,
"subdir": "linux-64",
"timestamp": 1664552878795,
"track_features": "",
"url": "https://repo.anaconda.com/pkgs/main/linux-64/pip-22.2.2-py38h06a4308_0.conda",
"version": "22.2.2"
}

View File

@@ -65,6 +65,9 @@ func NewDriver(libType string) (Driver, error) {
case ftypes.Cocoapods:
log.Logger.Warn("CocoaPods is supported for SBOM, not for vulnerability scanning")
return Driver{}, ErrSBOMSupportOnly
case ftypes.CondaPkg:
log.Logger.Warn("Conda package is supported for SBOM, not for vulnerability scanning")
return Driver{}, ErrSBOMSupportOnly
default:
return Driver{}, xerrors.Errorf("unsupported type %s", libType)
}

View File

@@ -6,6 +6,7 @@ import (
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/all"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/executable"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/c/conan"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/conda/meta"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/dart/pub"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/dotnet/deps"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/dotnet/nuget"

View File

@@ -61,6 +61,9 @@ const (
TypeNuget Type = "nuget"
TypeDotNetCore Type = "dotnet-core"
// Conda
TypeCondaPkg Type = "conda-pkg"
// Python
TypePythonPkg Type = "python-pkg"
TypePip Type = "pip"
@@ -132,7 +135,7 @@ var (
// TypeLanguages has all language analyzers
TypeLanguages = []Type{
TypeBundler, TypeGemSpec, TypeCargo, TypeComposer, TypeJar, TypePom, TypeGradleLock,
TypeNpmPkgLock, TypeNodePkg, TypeYarn, TypePnpm, TypeNuget, TypeDotNetCore,
TypeNpmPkgLock, TypeNodePkg, TypeYarn, TypePnpm, TypeNuget, TypeDotNetCore, TypeCondaPkg,
TypePythonPkg, TypePip, TypePipenv, TypePoetry, TypeGoBinary, TypeGoMod, TypeRustBinary, TypeConanLock,
TypeCocoaPods, TypePubSpecLock, TypeMixLock,
}
@@ -145,7 +148,7 @@ var (
}
// TypeIndividualPkgs has all analyzers for individual packages
TypeIndividualPkgs = []Type{TypeGemSpec, TypeNodePkg, TypePythonPkg, TypeGoBinary, TypeJar, TypeRustBinary}
TypeIndividualPkgs = []Type{TypeGemSpec, TypeNodePkg, TypeCondaPkg, TypePythonPkg, TypeGoBinary, TypeJar, TypeRustBinary}
// TypeConfigFiles has all config file analyzers
TypeConfigFiles = []Type{TypeYaml, TypeJSON, TypeDockerfile, TypeTerraform, TypeCloudFormation, TypeHelm}

View File

@@ -0,0 +1,46 @@
package meta
import (
"context"
"os"
"path/filepath"
"regexp"
"golang.org/x/xerrors"
"github.com/aquasecurity/go-dep-parser/pkg/conda/meta"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer/language"
"github.com/aquasecurity/trivy/pkg/fanal/types"
)
func init() {
analyzer.RegisterAnalyzer(&metaAnalyzer{})
}
const version = 1
var fileRegex = regexp.MustCompile(`.*/envs/.+/conda-meta/.+-.+-.+\.json`)
type metaAnalyzer struct{}
func (a metaAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
p := meta.NewParser()
libs, deps, err := p.Parse(input.Content)
if err != nil {
return nil, xerrors.Errorf("%s parse error: %w", input.FilePath, err)
}
return language.ToAnalysisResult(types.CondaPkg, input.FilePath, input.FilePath, libs, deps), nil
}
func (a metaAnalyzer) Required(filePath string, _ os.FileInfo) bool {
return fileRegex.MatchString(filepath.ToSlash(filePath))
}
func (a metaAnalyzer) Type() analyzer.Type {
return analyzer.TypeCondaPkg
}
func (a metaAnalyzer) Version() int {
return version
}

View File

@@ -0,0 +1,101 @@
package meta
import (
"context"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/fanal/types"
)
func Test_packagingAnalyzer_Analyze(t *testing.T) {
tests := []struct {
name string
inputFile string
want *analyzer.AnalysisResult
wantErr string
}{
{
name: "pip",
inputFile: "testdata/pip-22.2.2-py38h06a4308_0.json",
want: &analyzer.AnalysisResult{
Applications: []types.Application{
{
Type: types.CondaPkg,
FilePath: "testdata/pip-22.2.2-py38h06a4308_0.json",
Libraries: []types.Package{
{
Name: "pip",
Version: "22.2.2",
Licenses: []string{"MIT"},
FilePath: "testdata/pip-22.2.2-py38h06a4308_0.json",
},
},
},
},
},
},
{
name: "invalid",
inputFile: "testdata/invalid.json",
wantErr: "unable to parse conda package",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := os.Open(tt.inputFile)
require.NoError(t, err)
defer f.Close()
stat, err := f.Stat()
require.NoError(t, err)
a := metaAnalyzer{}
ctx := context.Background()
got, err := a.Analyze(ctx, analyzer.AnalysisInput{
FilePath: tt.inputFile,
Info: stat,
Content: f,
})
if tt.wantErr != "" {
assert.ErrorContains(t, err, tt.wantErr)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func Test_packagingAnalyzer_Required(t *testing.T) {
tests := []struct {
name string
filePath string
want bool
}{
{
name: "pip",
filePath: "/home/<user>/miniconda3/envs/<env>/conda-meta/pip-22.2.2-py38h06a4308_0.json",
want: true,
},
{
name: "invalid",
filePath: "/home/<user>/miniconda3/envs/<env>/conda-meta/invalid.json",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := metaAnalyzer{}
got := a.Required(tt.filePath, nil)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,62 @@
{
"build": "py38h06a4308_0",
"build_number": 0,
"channel": "https://repo.anaconda.com/pkgs/main/linux-64",
"constrains": [],
"depends": [
"python >=3.8,<3.9.0a0",
"setuptools",
"wheel"
],
"extracted_package_dir": "/home/user/miniconda3/pkgs/pip-22.2.2-py38h06a4308_0",
"features": "",
"files": [
"bin/pip",
"<SNIP>",
"lib/python3.8/site-packages/pip/py.typed"
],
"fn": "pip-22.2.2-py38h06a4308_0.conda",
"legacy_bz2_md5": "2ac9f1cfec65a1e4ef00cc0132ecd753",
"legacy_bz2_size": 2849993,
"license": "MIT",
"license_family": "MIT",
"link": {
"source": "/home/user/miniconda3/pkgs/pip-22.2.2-py38h06a4308_0",
"type": 1
},
"md5": "ed3e0331e7c614b3148c9911e1fc15e3",
"name": "pip",
"package_tarball_full_path": "/home/user/miniconda3/pkgs/pip-22.2.2-py38h06a4308_0.conda",
"paths_data": {
"paths": [
{
"_path": "bin/pip",
"file_mode": "text",
"path_type": "hardlink",
"prefix_placeholder": "/opt/conda/conda-bld/pip_1664552683240/_h_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold",
"sha256": "1110c03ca2fb86e43e8b52a61cd9d7c722b2817fc893a613e1a947f5d7049008",
"sha256_in_prefix": "14ed5ba79d096035b83a469e863acbbaadd3ea6ca5f23f79eefa91fa857d3f19",
"size_in_bytes": 475
},
{
"_path": "<SNIP>"
},
{
"_path": "lib/python3.8/site-packages/pip/py.typed",
"path_type": "hardlink",
"sha256": "10156fbcf4539ff788a73e5ee50ced48276b317ed0c1ded53fddd14a82256762",
"sha256_in_prefix": "10156fbcf4539ff788a73e5ee50ced48276b317ed0c1ded53fddd14a82256762",
"size_in_bytes": 286
}
],
"paths_version": 1
},
"requested_spec": "None",
"sha256": "3fb76b94cfa5ea9732bfb241b3d234ec0a5a48d16755c3c1ef3c94630f91eb26",
"size": 2417732,
"subdir": "linux-64",
"timestamp": 1664552878795,
"track_features": "",
"url": "https://repo.anaconda.com/pkgs/main/linux-64/pip-22.2.2-py38h06a4308_0.conda",
"version": "22.2.2"
}

View File

@@ -232,12 +232,13 @@ func ApplyLayers(layers []types.BlobInfo) types.ArtifactDetail {
return mergedLayer
}
// aggregate merges all packages installed by pip/gem/npm/jar into each application
// aggregate merges all packages installed by pip/gem/npm/jar/conda into each application
func aggregate(detail *types.ArtifactDetail) {
var apps []types.Application
aggregatedApps := map[string]*types.Application{
types.PythonPkg: {Type: types.PythonPkg},
types.CondaPkg: {Type: types.CondaPkg},
types.GemSpec: {Type: types.GemSpec},
types.NodePkg: {Type: types.NodePkg},
types.Jar: {Type: types.Jar},

View File

@@ -36,6 +36,9 @@ var (
// python
types.PythonPkg,
// conda
types.CondaPkg,
// node.js
types.NodePkg,

View File

@@ -17,6 +17,7 @@ const (
Pip = "pip"
Pipenv = "pipenv"
Poetry = "poetry"
CondaPkg = "conda-pkg"
PythonPkg = "python-pkg"
NodePkg = "node-pkg"
Yarn = "yarn"

View File

@@ -85,6 +85,8 @@ func (p *PackageURL) PackageType() string {
return ftypes.Jar
case packageurl.TypeGem:
return ftypes.GemSpec
case packageurl.TypeConda:
return ftypes.CondaPkg
case packageurl.TypePyPi:
return ftypes.PythonPkg
case packageurl.TypeGolang:
@@ -305,6 +307,8 @@ func purlType(t string) string {
return packageurl.TypeGem
case ftypes.NuGet, ftypes.DotNetCore:
return packageurl.TypeNuget
case ftypes.CondaPkg:
return packageurl.TypeConda
case ftypes.PythonPkg, ftypes.Pip, ftypes.Pipenv, ftypes.Poetry:
return packageurl.TypePyPi
case ftypes.GoBinary, ftypes.GoModule:

View File

@@ -134,6 +134,21 @@ func TestNewPackageURL(t *testing.T) {
},
},
},
{
name: "conda package",
typ: ftypes.CondaPkg,
pkg: ftypes.Package{
Name: "absl-py",
Version: "0.4.1",
},
want: purl.PackageURL{
PackageURL: packageurl.PackageURL{
Type: packageurl.TypeConda,
Name: "absl-py",
Version: "0.4.1",
},
},
},
{
name: "composer package",
typ: ftypes.Composer,
@@ -451,6 +466,18 @@ func TestFromString(t *testing.T) {
},
},
},
{
name: "happy path for conda",
purl: "pkg:conda/absl-py@0.4.1",
want: purl.PackageURL{
PackageURL: packageurl.PackageURL{
Type: packageurl.TypeConda,
Name: "absl-py",
Version: "0.4.1",
Qualifiers: packageurl.Qualifiers{},
},
},
},
{
name: "bad rpm",
purl: "pkg:rpm/redhat/a--@1.0.0",

View File

@@ -249,7 +249,7 @@ func (e *Marshaler) marshalComponents(r types.Report, bomRef string) (*[]cdx.Com
}
if result.Type == ftypes.NodePkg || result.Type == ftypes.PythonPkg ||
result.Type == ftypes.GemSpec || result.Type == ftypes.Jar {
result.Type == ftypes.GemSpec || result.Type == ftypes.Jar || result.Type == ftypes.CondaPkg {
// If a package is language-specific package that isn't associated with a lock file,
// it will be a dependency of a component under "metadata".
// e.g.

View File

@@ -143,7 +143,7 @@ func initApplication(pkg spdx.Package2_2) *ftypes.Application {
FilePath: pkg.PackageSourceInfo,
}
if pkg.PackageName == ftypes.NodePkg || pkg.PackageName == ftypes.PythonPkg ||
pkg.PackageName == ftypes.GemSpec || pkg.PackageName == ftypes.Jar {
pkg.PackageName == ftypes.GemSpec || pkg.PackageName == ftypes.Jar || pkg.PackageName == ftypes.CondaPkg {
app.FilePath = ""
}
return app

View File

@@ -31,6 +31,7 @@ import (
var (
pkgTargets = map[string]string{
ftypes.PythonPkg: "Python",
ftypes.CondaPkg: "Conda",
ftypes.GemSpec: "Ruby",
ftypes.NodePkg: "Node.js",
ftypes.Jar: "Java",