Add relationships for dpkg packages (#2212)

* add relationships for deb packages

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

* update snapshots

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

* bump json schema

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

* small refactor to remove duplicate code

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 2023-10-11 08:56:26 -04:00 committed by GitHub
parent 0748945c83
commit ef759038f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 2541 additions and 42 deletions

View File

@ -3,5 +3,5 @@ package internal
const ( const (
// JSONSchemaVersion is the current schema version output by the JSON encoder // 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. // 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 = "11.0.0" JSONSchemaVersion = "11.0.1"
) )

File diff suppressed because it is too large Load Diff

View File

@ -57,7 +57,7 @@ func redactor(values ...string) testutils.Redactor {
`sha256:[A-Za-z0-9]{64}`: `sha256:redacted`, `sha256:[A-Za-z0-9]{64}`: `sha256:redacted`,
// BOM refs // BOM refs
`bom-ref="[a-zA-Z0-9\-:]+"`: `bom-ref:redacted`, `bom-ref="[a-zA-Z0-9\-:]+"`: `bom-ref="redacted"`,
}, },
) )
} }

View File

@ -32,7 +32,7 @@
<property name="syft:location:0:path">/some/path/pkg1</property> <property name="syft:location:0:path">/some/path/pkg1</property>
</properties> </properties>
</component> </component>
<component bom-ref="pkg:deb/debian/package-2@2.0.1?package-id=db4abfe497c180d3" type="library"> <component bom-ref="pkg:deb/debian/package-2@2.0.1?package-id=ad5013466727018f" type="library">
<name>package-2</name> <name>package-2</name>
<version>2.0.1</version> <version>2.0.1</version>
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe> <cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>

View File

@ -34,7 +34,7 @@
<property name="syft:location:0:path">/somefile-1.txt</property> <property name="syft:location:0:path">/somefile-1.txt</property>
</properties> </properties>
</component> </component>
<component bom-ref="pkg:deb/debian/package-2@2.0.1?package-id=958443e2d9304af4" type="library"> <component bom-ref="pkg:deb/debian/package-2@2.0.1?package-id=f27313b22a5ba330" type="library">
<name>package-2</name> <name>package-2</name>
<version>2.0.1</version> <version>2.0.1</version>
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe> <cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>

View File

@ -39,7 +39,7 @@
}, },
{ {
"name": "package-2", "name": "package-2",
"SPDXID": "SPDXRef-Package-deb-package-2-db4abfe497c180d3", "SPDXID": "SPDXRef-Package-deb-package-2-ad5013466727018f",
"versionInfo": "2.0.1", "versionInfo": "2.0.1",
"supplier": "NOASSERTION", "supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION", "downloadLocation": "NOASSERTION",
@ -78,7 +78,7 @@
}, },
{ {
"spdxElementId": "SPDXRef-DocumentRoot-Directory-some-path", "spdxElementId": "SPDXRef-DocumentRoot-Directory-some-path",
"relatedSpdxElement": "SPDXRef-Package-deb-package-2-db4abfe497c180d3", "relatedSpdxElement": "SPDXRef-Package-deb-package-2-ad5013466727018f",
"relationshipType": "CONTAINS" "relationshipType": "CONTAINS"
}, },
{ {

View File

@ -39,7 +39,7 @@
}, },
{ {
"name": "package-2", "name": "package-2",
"SPDXID": "SPDXRef-Package-deb-package-2-958443e2d9304af4", "SPDXID": "SPDXRef-Package-deb-package-2-f27313b22a5ba330",
"versionInfo": "2.0.1", "versionInfo": "2.0.1",
"supplier": "NOASSERTION", "supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION", "downloadLocation": "NOASSERTION",
@ -92,7 +92,7 @@
}, },
{ {
"spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input", "spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input",
"relatedSpdxElement": "SPDXRef-Package-deb-package-2-958443e2d9304af4", "relatedSpdxElement": "SPDXRef-Package-deb-package-2-f27313b22a5ba330",
"relationshipType": "CONTAINS" "relationshipType": "CONTAINS"
}, },
{ {

View File

@ -39,7 +39,7 @@
}, },
{ {
"name": "package-2", "name": "package-2",
"SPDXID": "SPDXRef-Package-deb-package-2-958443e2d9304af4", "SPDXID": "SPDXRef-Package-deb-package-2-f27313b22a5ba330",
"versionInfo": "2.0.1", "versionInfo": "2.0.1",
"supplier": "NOASSERTION", "supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION", "downloadLocation": "NOASSERTION",
@ -214,7 +214,7 @@
}, },
{ {
"spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input", "spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input",
"relatedSpdxElement": "SPDXRef-Package-deb-package-2-958443e2d9304af4", "relatedSpdxElement": "SPDXRef-Package-deb-package-2-f27313b22a5ba330",
"relationshipType": "CONTAINS" "relationshipType": "CONTAINS"
}, },
{ {

View File

@ -61,7 +61,7 @@ ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256:2731251dc34951
##### Package: package-2 ##### Package: package-2
PackageName: package-2 PackageName: package-2
SPDXID: SPDXRef-Package-deb-package-2-958443e2d9304af4 SPDXID: SPDXRef-Package-deb-package-2-f27313b22a5ba330
PackageVersion: 2.0.1 PackageVersion: 2.0.1
PackageSupplier: NOASSERTION PackageSupplier: NOASSERTION
PackageDownloadLocation: NOASSERTION PackageDownloadLocation: NOASSERTION
@ -97,6 +97,6 @@ Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef
Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-d1-f3-c6f5b29dca12661f Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-d1-f3-c6f5b29dca12661f
Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-f2-f9e49132a4b96ccd Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-f2-f9e49132a4b96ccd
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-125840abc1c66dd7 Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-125840abc1c66dd7
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-958443e2d9304af4 Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-f27313b22a5ba330
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Image-user-image-input Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Image-user-image-input

View File

@ -20,7 +20,7 @@ FilesAnalyzed: false
##### Package: package-2 ##### Package: package-2
PackageName: package-2 PackageName: package-2
SPDXID: SPDXRef-Package-deb-package-2-db4abfe497c180d3 SPDXID: SPDXRef-Package-deb-package-2-ad5013466727018f
PackageVersion: 2.0.1 PackageVersion: 2.0.1
PackageSupplier: NOASSERTION PackageSupplier: NOASSERTION
PackageDownloadLocation: NOASSERTION PackageDownloadLocation: NOASSERTION
@ -50,6 +50,6 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-2
##### Relationships ##### Relationships
Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-python-package-1-9265397e5e15168a Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-python-package-1-9265397e5e15168a
Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-deb-package-2-db4abfe497c180d3 Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-deb-package-2-ad5013466727018f
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Directory-some-path Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Directory-some-path

View File

@ -23,7 +23,7 @@ ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256:2731251dc34951
##### Package: package-2 ##### Package: package-2
PackageName: package-2 PackageName: package-2
SPDXID: SPDXRef-Package-deb-package-2-958443e2d9304af4 SPDXID: SPDXRef-Package-deb-package-2-f27313b22a5ba330
PackageVersion: 2.0.1 PackageVersion: 2.0.1
PackageSupplier: NOASSERTION PackageSupplier: NOASSERTION
PackageDownloadLocation: NOASSERTION PackageDownloadLocation: NOASSERTION
@ -53,6 +53,6 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-1
##### Relationships ##### Relationships
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-125840abc1c66dd7 Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-125840abc1c66dd7
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-958443e2d9304af4 Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-f27313b22a5ba330
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Image-user-image-input Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Image-user-image-input

View File

@ -41,7 +41,7 @@
} }
}, },
{ {
"id": "db4abfe497c180d3", "id": "ad5013466727018f",
"name": "package-2", "name": "package-2",
"version": "2.0.1", "version": "2.0.1",
"type": "deb", "type": "deb",

View File

@ -36,7 +36,7 @@
} }
}, },
{ {
"id": "9fd0b9f41034991d", "id": "aa0ca2c331576dfd",
"name": "package-2", "name": "package-2",
"version": "2.0.1", "version": "2.0.1",
"type": "deb", "type": "deb",

View File

@ -37,7 +37,7 @@
} }
}, },
{ {
"id": "958443e2d9304af4", "id": "f27313b22a5ba330",
"name": "package-2", "name": "package-2",
"version": "2.0.1", "version": "2.0.1",
"type": "deb", "type": "deb",

View File

@ -44,6 +44,11 @@ func TestDpkgCataloger(t *testing.T) {
Contains configuration files and directories required for Contains configuration files and directories required for
authentication to work on Debian systems. This package is required authentication to work on Debian systems. This package is required
on almost all installations.`, on almost all installations.`,
Depends: []string{
"debconf (>= 0.5) | debconf-2.0",
"debconf (>= 1.5.19) | cdebconf",
"libpam-modules (>= 1.0.1-6)",
},
Files: []pkg.DpkgFileRecord{ Files: []pkg.DpkgFileRecord{
{ {
Path: "/etc/pam.conf", Path: "/etc/pam.conf",
@ -112,6 +117,7 @@ func TestDpkgCataloger(t *testing.T) {
SQLite is a C library that implements an SQL database engine. SQLite is a C library that implements an SQL database engine.
Programs that link with the SQLite library can have SQL database Programs that link with the SQLite library can have SQL database
access without running a separate RDBMS process.`, access without running a separate RDBMS process.`,
Depends: []string{"libc6 (>= 2.29)"},
Files: []pkg.DpkgFileRecord{ Files: []pkg.DpkgFileRecord{
{Path: "/usr/lib/aarch64-linux-gnu/libsqlite3.so.0.8.6", Digest: &file.Digest{ {Path: "/usr/lib/aarch64-linux-gnu/libsqlite3.so.0.8.6", Digest: &file.Digest{
Algorithm: "md5", Algorithm: "md5",

View File

@ -36,6 +36,7 @@ func newDpkgPackage(d pkg.DpkgMetadata, dbLocation file.Location, resolver file.
Metadata: d, Metadata: d,
} }
if resolver != nil {
// the current entry only has what may have been listed in the status file, however, there are additional // the current entry only has what may have been listed in the status file, however, there are additional
// files that are listed in multiple other locations. We should retrieve them all and merge the file lists // files that are listed in multiple other locations. We should retrieve them all and merge the file lists
// together. // together.
@ -43,6 +44,7 @@ func newDpkgPackage(d pkg.DpkgMetadata, dbLocation file.Location, resolver file.
// fetch additional data from the copyright file to derive the license information // fetch additional data from the copyright file to derive the license information
addLicenses(resolver, dbLocation, &p) addLicenses(resolver, dbLocation, &p)
}
p.SetID() p.SetID()

View File

@ -35,7 +35,7 @@ func parseDpkgDB(resolver file.Resolver, env *generic.Environment, reader file.L
pkgs = append(pkgs, newDpkgPackage(m, reader.Location, resolver, env.LinuxRelease)) pkgs = append(pkgs, newDpkgPackage(m, reader.Location, resolver, env.LinuxRelease))
} }
return pkgs, nil, nil return pkgs, associateRelationships(pkgs), nil
} }
// parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed. // parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed.
@ -63,6 +63,22 @@ func parseDpkgStatus(reader io.Reader) ([]pkg.DpkgMetadata, error) {
return metadata, nil return metadata, nil
} }
// dpkgExtractedMetadata is an adapter struct to capture the fields from the dpkg status file, however, the final
// pkg.DpkgMetadata struct has different types for some fields (e.g. Provides, Depends, and PreDepends is []string, not a string).
type dpkgExtractedMetadata struct {
Package string `mapstructure:"Package"`
Source string `mapstructure:"Source"`
Version string `mapstructure:"Version"`
SourceVersion string `mapstructure:"SourceVersion"`
Architecture string `mapstructure:"Architecture"`
Maintainer string `mapstructure:"Maintainer"`
InstalledSize int `mapstructure:"InstalledSize"`
Description string `mapstructure:"Description"`
Provides string `mapstructure:"Provides"`
Depends string `mapstructure:"Depends"`
PreDepends string `mapstructure:"PreDepends"` // note: original doc is Pre-Depends
}
// parseDpkgStatusEntry returns an individual Dpkg entry, or returns errEndOfPackages if there are no more packages to parse from the reader. // parseDpkgStatusEntry returns an individual Dpkg entry, or returns errEndOfPackages if there are no more packages to parse from the reader.
func parseDpkgStatusEntry(reader *bufio.Reader) (*pkg.DpkgMetadata, error) { func parseDpkgStatusEntry(reader *bufio.Reader) (*pkg.DpkgMetadata, error) {
var retErr error var retErr error
@ -77,22 +93,36 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (*pkg.DpkgMetadata, error) {
retErr = err retErr = err
} }
entry := pkg.DpkgMetadata{} raw := dpkgExtractedMetadata{}
err = mapstructure.Decode(dpkgFields, &entry) err = mapstructure.Decode(dpkgFields, &raw)
if err != nil { if err != nil {
return nil, err return nil, err
} }
sourceName, sourceVersion := extractSourceVersion(entry.Source) sourceName, sourceVersion := extractSourceVersion(raw.Source)
if sourceVersion != "" { if sourceVersion != "" {
entry.SourceVersion = sourceVersion raw.SourceVersion = sourceVersion
entry.Source = sourceName raw.Source = sourceName
} }
if entry.Package == "" { if raw.Package == "" {
return nil, retErr return nil, retErr
} }
entry := pkg.DpkgMetadata{
Package: raw.Package,
Source: raw.Source,
Version: raw.Version,
SourceVersion: raw.SourceVersion,
Architecture: raw.Architecture,
Maintainer: raw.Maintainer,
InstalledSize: raw.InstalledSize,
Description: raw.Description,
Provides: splitPkgList(raw.Provides),
Depends: splitPkgList(raw.Depends),
PreDepends: splitPkgList(raw.PreDepends),
}
// there may be an optional conffiles section that we should persist as files // there may be an optional conffiles section that we should persist as files
if conffilesSection, exists := dpkgFields["Conffiles"]; exists && conffilesSection != nil { if conffilesSection, exists := dpkgFields["Conffiles"]; exists && conffilesSection != nil {
if sectionStr, ok := conffilesSection.(string); ok { if sectionStr, ok := conffilesSection.(string); ok {
@ -108,6 +138,17 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (*pkg.DpkgMetadata, error) {
return &entry, retErr return &entry, retErr
} }
func splitPkgList(pkgList string) (ret []string) {
fields := strings.Split(pkgList, ",")
for _, field := range fields {
field = strings.TrimSpace(field)
if field != "" {
ret = append(ret, field)
}
}
return ret
}
func extractAllFields(reader *bufio.Reader) (map[string]interface{}, error) { func extractAllFields(reader *bufio.Reader) (map[string]interface{}, error) {
dpkgFields := make(map[string]interface{}) dpkgFields := make(map[string]interface{})
var key string var key string
@ -195,3 +236,79 @@ func handleNewKeyValue(line string) (key string, val interface{}, err error) {
return "", nil, fmt.Errorf("cannot parse field from line: '%s'", line) return "", nil, fmt.Errorf("cannot parse field from line: '%s'", line)
} }
// associateRelationships will create relationships between packages based on the "Depends", "Pre-Depends", and "Provides"
// fields for installed packages. if there is an installed package that has a dependency that is (somehow) not installed,
// then that relationship (between the installed and uninstalled package) will NOT be created.
func associateRelationships(pkgs []pkg.Package) (relationships []artifact.Relationship) {
// map["provides" + "package"] -> packages that provide that package
lookup := make(map[string][]pkg.Package)
// read provided and add as keys for lookup keys as well as package names
for _, p := range pkgs {
meta, ok := p.Metadata.(pkg.DpkgMetadata)
if !ok {
log.Warnf("cataloger failed to extract dpkg 'provides' metadata for package %+v", p.Name)
continue
}
lookup[p.Name] = append(lookup[p.Name], p)
for _, provides := range meta.Provides {
k := stripVersionSpecifier(provides)
lookup[k] = append(lookup[k], p)
}
}
// read "Depends" and "Pre-Depends" and match with keys
for _, p := range pkgs {
meta, ok := p.Metadata.(pkg.DpkgMetadata)
if !ok {
log.Warnf("cataloger failed to extract dpkg 'dependency' metadata for package %+v", p.Name)
continue
}
var allDeps []string
allDeps = append(allDeps, meta.Depends...)
allDeps = append(allDeps, meta.PreDepends...)
for _, depSpecifier := range allDeps {
deps := splitPackageChoice(depSpecifier)
for _, dep := range deps {
for _, depPkg := range lookup[dep] {
relationships = append(relationships, artifact.Relationship{
From: depPkg,
To: p,
Type: artifact.DependencyOfRelationship,
})
}
}
}
}
return relationships
}
func stripVersionSpecifier(s string) string {
// examples:
// libgmp10 (>= 2:6.2.1+dfsg1) --> libgmp10
// libgmp10 --> libgmp10
// foo [i386] --> foo
// default-mta | mail-transport-agent --> default-mta | mail-transport-agent
// kernel-headers-2.2.10 [!hurd-i386] --> kernel-headers-2.2.10
items := internal.SplitAny(s, "[(<>=")
if len(items) == 0 {
return s
}
return strings.TrimSpace(items[0])
}
func splitPackageChoice(s string) (ret []string) {
fields := strings.Split(s, "|")
for _, field := range fields {
field = strings.TrimSpace(field)
if field != "" {
ret = append(ret, stripVersionSpecifier(field))
}
}
return ret
}

View File

@ -11,9 +11,11 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
) )
@ -48,6 +50,18 @@ func Test_parseDpkgStatus(t *testing.T) {
* apt-cdrom to use removable media as a source for packages * apt-cdrom to use removable media as a source for packages
* apt-config as an interface to the configuration settings * apt-config as an interface to the configuration settings
* apt-key as an interface to manage authentication keys`, * apt-key as an interface to manage authentication keys`,
Provides: []string{"apt-transport-https (= 1.8.2)"},
Depends: []string{
"adduser",
"gpgv | gpgv2 | gpgv1",
"debian-archive-keyring",
"libapt-pkg5.0 (>= 1.7.0~alpha3~)",
"libc6 (>= 2.15)",
"libgcc1 (>= 1:3.0)",
"libgnutls30 (>= 3.6.6)",
"libseccomp2 (>= 1.0.1)",
"libstdc++6 (>= 5.2)",
},
Files: []pkg.DpkgFileRecord{ Files: []pkg.DpkgFileRecord{
{ {
Path: "/etc/apt/apt.conf.d/01autoremove", Path: "/etc/apt/apt.conf.d/01autoremove",
@ -110,6 +124,18 @@ func Test_parseDpkgStatus(t *testing.T) {
* apt-cdrom to use removable media as a source for packages * apt-cdrom to use removable media as a source for packages
* apt-config as an interface to the configuration settings * apt-config as an interface to the configuration settings
* apt-key as an interface to manage authentication keys`, * apt-key as an interface to manage authentication keys`,
Provides: []string{"apt-transport-https (= 1.8.2)"},
Depends: []string{
"adduser",
"gpgv | gpgv2 | gpgv1",
"debian-archive-keyring",
"libapt-pkg5.0 (>= 1.7.0~alpha3~)",
"libc6 (>= 2.15)",
"libgcc1 (>= 1:3.0)",
"libgnutls30 (>= 3.6.6)",
"libseccomp2 (>= 1.0.1)",
"libstdc++6 (>= 5.2)",
},
Files: []pkg.DpkgFileRecord{}, Files: []pkg.DpkgFileRecord{},
}, },
}, },
@ -135,6 +161,8 @@ func Test_parseDpkgStatus(t *testing.T) {
globe. It is updated periodically to reflect changes made by globe. It is updated periodically to reflect changes made by
political bodies to time zone boundaries, UTC offsets, and political bodies to time zone boundaries, UTC offsets, and
daylight-saving rules.`, daylight-saving rules.`,
Provides: []string{"tzdata-buster"},
Depends: []string{"debconf (>= 0.5) | debconf-2.0"},
Files: []pkg.DpkgFileRecord{}, Files: []pkg.DpkgFileRecord{},
}, },
{ {
@ -149,6 +177,14 @@ func Test_parseDpkgStatus(t *testing.T) {
important utilities included in this package allow you to view kernel important utilities included in this package allow you to view kernel
messages, create new filesystems, view block device information, messages, create new filesystems, view block device information,
interface with real time clock, etc.`, interface with real time clock, etc.`,
Depends: []string{"fdisk", "login (>= 1:4.5-1.1~)"},
PreDepends: []string{
"libaudit1 (>= 1:2.2.1)", "libblkid1 (>= 2.31.1)", "libc6 (>= 2.25)",
"libcap-ng0 (>= 0.7.9)", "libmount1 (>= 2.25)", "libpam0g (>= 0.99.7.1)",
"libselinux1 (>= 2.6-3~)", "libsmartcols1 (>= 2.33)", "libsystemd0",
"libtinfo6 (>= 6)", "libudev1 (>= 183)", "libuuid1 (>= 2.16)",
"zlib1g (>= 1:1.1.4)",
},
Files: []pkg.DpkgFileRecord{ Files: []pkg.DpkgFileRecord{
{ {
Path: "/etc/default/hwclock", Path: "/etc/default/hwclock",
@ -397,3 +433,122 @@ func Test_handleNewKeyValue(t *testing.T) {
}) })
} }
} }
func Test_stripVersionSpecifier(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "package name only",
input: "test",
want: "test",
},
{
name: "with version",
input: "test (1.2.3)",
want: "test",
},
{
name: "multiple packages",
input: "test | other",
want: "test | other",
},
{
name: "with architecture specifiers",
input: "test [amd64 i386]",
want: "test",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, stripVersionSpecifier(tt.input))
})
}
}
func Test_associateRelationships(t *testing.T) {
tests := []struct {
name string
fixture string
wantRelationships map[string][]string
}{
{
name: "relationships for coreutils",
fixture: "test-fixtures/status/coreutils-relationships",
wantRelationships: map[string][]string{
"coreutils": {"libacl1", "libattr1", "libc6", "libgmp10", "libselinux1"},
"libacl1": {"libc6"},
"libattr1": {"libc6"},
"libc6": {"libgcc-s1"},
"libgcc-s1": {"gcc-12-base", "libc6"},
"libgmp10": {"libc6"},
"libpcre2-8-0": {"libc6"},
"libselinux1": {"libc6", "libpcre2-8-0"},
},
},
{
name: "relationships from dpkg example docs",
fixture: "test-fixtures/status/doc-examples",
wantRelationships: map[string][]string{
"made-up-package-1": {"kernel-headers-2.2.10", "hurd-dev", "gnumach-dev"},
"made-up-package-2": {"libluajit5.1-dev", "liblua5.1-dev"},
"made-up-package-3": {"foo", "bar"},
// note that the "made-up-package-4" depends on "made-up-package-5" but not via the direct
// package name, but through the "provides" virtual package name "virtual-package-5".
"made-up-package-4": {"made-up-package-5"},
// note that though there is a "default-mta | mail-transport-agent | not-installed"
// dependency choice we raise up the packages that are installed for every choice.
// In this case that means that "default-mta" and "mail-transport-agent".
"mutt": {"libc6", "default-mta", "mail-transport-agent"},
},
},
{
name: "relationships for libpam-runtime",
fixture: "test-fixtures/status/libpam-runtime",
wantRelationships: map[string][]string{
"libpam-runtime": {"debconf1", "debconf-2.0", "debconf2", "cdebconf", "libpam-modules"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := os.Open(tt.fixture)
require.NoError(t, err)
reader := file.NewLocationReadCloser(file.NewLocation(tt.fixture), f)
pkgs, relationships, err := parseDpkgDB(nil, &generic.Environment{}, reader)
require.NotEmpty(t, pkgs)
require.NotEmpty(t, relationships)
require.NoError(t, err)
if d := cmp.Diff(tt.wantRelationships, abstractRelationships(t, relationships)); d != "" {
t.Errorf("unexpected relationships (-want +got):\n%s", d)
}
})
}
}
func abstractRelationships(t testing.TB, relationships []artifact.Relationship) map[string][]string {
t.Helper()
abstracted := make(map[string][]string)
for _, relationship := range relationships {
fromPkg, ok := relationship.From.(pkg.Package)
if !ok {
continue
}
toPkg, ok := relationship.To.(pkg.Package)
if !ok {
continue
}
// we build this backwards since we use DependencyOfRelationship instead of DependsOn
abstracted[toPkg.Name] = append(abstracted[toPkg.Name], fromPkg.Name)
}
return abstracted
}

View File

@ -0,0 +1,114 @@
Package: coreutils
Essential: yes
Status: install ok installed
Priority: required
Section: utils
Installed-Size: 20272
Maintainer: Michael Stone <mstone@debian.org>
Architecture: arm64
Multi-Arch: foreign
Version: 9.1-1
Pre-Depends: libacl1 (>= 2.2.23), libattr1 (>= 1:2.4.44), libc6 (>= 2.34), libgmp10 (>= 2:6.2.1+dfsg1), libselinux1 (>= 3.1~)
Package: libacl1
Status: install ok installed
Priority: optional
Section: libs
Installed-Size: 101
Maintainer: Guillem Jover <guillem@debian.org>
Architecture: arm64
Multi-Arch: same
Source: acl
Version: 2.3.1-3
Depends: libc6 (>= 2.33)
Package: libc6
Status: install ok installed
Priority: optional
Section: libs
Installed-Size: 23127
Maintainer: GNU Libc Maintainers <debian-glibc@lists.debian.org>
Architecture: arm64
Multi-Arch: same
Source: glibc
Version: 2.36-9+deb12u1
Depends: libgcc-s1
Recommends: libidn2-0 (>= 2.0.5~)
Suggests: glibc-doc, debconf | debconf-2.0, libc-l10n, locales, libnss-nis, libnss-nisplus
Breaks: aide (<< 0.17.3-4+b3), busybox (<< 1.30.1-6), chrony (<< 4.2-3~), fakechroot (<< 2.19-3.5), firefox (<< 91~), firefox-esr (<< 91~), gnumach-image-1.8-486 (<< 2:1.8+git20210923~), gnumach-image-1.8-486-dbg (<< 2:1.8+git20210923~), gnumach-image-1.8-xen-486 (<< 2:1.8+git20210923~), gnumach-image-1.8-xen-486-dbg (<< 2:1.8+git20210923~), hurd (<< 1:0.9.git20220301-2), ioquake3 (<< 1.36+u20200211.f2c61c1~dfsg-2~), iraf-fitsutil (<< 2018.07.06-4), libgegl-0.4-0 (<< 0.4.18), libtirpc1 (<< 0.2.3), locales (<< 2.36), locales-all (<< 2.36), macs (<< 2.2.7.1-3~), nocache (<< 1.1-1~), nscd (<< 2.36), openarena (<< 0.8.8+dfsg-4~), openssh-server (<< 1:8.1p1-5), python3-iptables (<< 1.0.0-2), r-cran-later (<< 0.7.5+dfsg-2), tinydns (<< 1:1.05-14), valgrind (<< 1:3.19.0-1~), wcc (<< 0.0.2+dfsg-3)
Package: libgcc-s1
Protected: yes
Status: install ok installed
Priority: optional
Section: libs
Installed-Size: 147
Maintainer: Debian GCC Maintainers <debian-gcc@lists.debian.org>
Architecture: arm64
Multi-Arch: same
Source: gcc-12
Version: 12.2.0-14
Replaces: libgcc1 (<< 1:10)
Provides: libgcc1 (= 1:12.2.0-14)
Depends: gcc-12-base (= 12.2.0-14), libc6 (>= 2.35)
Package: gcc-12-base
Status: install ok installed
Priority: optional
Section: libs
Installed-Size: 100
Maintainer: Debian GCC Maintainers <debian-gcc@lists.debian.org>
Architecture: arm64
Multi-Arch: same
Source: gcc-12
Version: 12.2.0-14
Breaks: gnat (<< 7)
Package: libattr1
Status: install ok installed
Priority: optional
Section: libs
Installed-Size: 99
Maintainer: Guillem Jover <guillem@debian.org>
Architecture: arm64
Multi-Arch: same
Source: attr
Version: 1:2.5.1-4
Depends: libc6 (>= 2.17)
Package: libgmp10
Status: install ok installed
Priority: optional
Section: libs
Installed-Size: 855
Maintainer: Debian Science Team <debian-science-maintainers@lists.alioth.debian.org>
Architecture: arm64
Multi-Arch: same
Source: gmp
Version: 2:6.2.1+dfsg1-1.1
Depends: libc6 (>= 2.17)
Breaks: libmath-gmp-perl (<< 2.20-1), libmath-prime-util-gmp-perl (<< 0.51-2), postgresql-pgmp (<< 1.0.3-1)
Package: libselinux1
Status: install ok installed
Priority: optional
Section: libs
Installed-Size: 223
Maintainer: Debian SELinux maintainers <selinux-devel@lists.alioth.debian.org>
Architecture: arm64
Multi-Arch: same
Source: libselinux (3.4-1)
Version: 3.4-1+b6
Depends: libc6 (>= 2.34), libpcre2-8-0 (>= 10.22)
Package: libpcre2-8-0
Status: install ok installed
Priority: optional
Section: libs
Installed-Size: 649
Maintainer: Matthew Vernon <matthew@debian.org>
Architecture: arm64
Multi-Arch: same
Source: pcre2
Version: 10.42-1
Depends: libc6 (>= 2.34)

View File

@ -0,0 +1,46 @@
Package: mutt
Version: 1.3.17-1
Depends: libc6 (>= 2.2.1), default-mta | mail-transport-agent | not-installed
Package: made-up-package-1
Version: 1.0.0
Source: glibc
Depends: kernel-headers-2.2.10 [!hurd-i386],
hurd-dev [hurd-i386], gnumach-dev [hurd-i386]
Package: made-up-package-2
Version: 2.0.0
Depends:
libluajit5.1-dev [i386 amd64 kfreebsd-i386 armel armhf powerpc mips],
liblua5.1-dev [hurd-i386 ia64 kfreebsd-amd64 s390x sparc],
Package: made-up-package-3
Version: 3.0.0
Depends: foo [i386], bar [amd64]
Package: made-up-package-4
Version: 3.0.0
Depends: virtual-package-5
Package: made-up-package-5
Provides: virtual-package-5 (=1.0)
Package: foo
Package: bar
Package: kernel-headers-2.2.10
Package: hurd-dev
Package: gnumach-dev
Package: default-mta
Package: mail-transport-agent
Package: libc6
Package: libluajit5.1-dev
Package: liblua5.1-dev

View File

@ -0,0 +1,23 @@
Package: libpam-runtime
Status: install ok installed
Priority: required
Section: admin
Installed-Size: 876
Maintainer: Sam Hartman <hartmans@debian.org>
Architecture: all
Multi-Arch: foreign
Source: pam
Version: 1.5.2-6+deb12u1
Replaces: libpam0g-dev, libpam0g-util
Depends: debconf1 (>= 0.5) | debconf-2.0, debconf2 (>= 1.5.19) | cdebconf, libpam-modules (>= 1.0.1-6)
Conflicts: libpam0g-util
Package: debconf1
Package: debconf2
Package: debconf-2.0
Package: cdebconf
Package: libpam-modules

View File

@ -14,15 +14,48 @@ var _ FileOwner = (*DpkgMetadata)(nil)
// DpkgMetadata represents all captured data for a Debian package DB entry; available fields are described // DpkgMetadata represents all captured data for a Debian package DB entry; available fields are described
// at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section. // at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section.
// Additional information about how these fields are used can be found at
// - https://www.debian.org/doc/debian-policy/ch-controlfields.html
// - https://www.debian.org/doc/debian-policy/ch-relationships.html
// - https://www.debian.org/doc/debian-policy/ch-binary.html#s-virtual-pkg
// - https://www.debian.org/doc/debian-policy/ch-relationships.html#s-virtual
type DpkgMetadata struct { type DpkgMetadata struct {
Package string `mapstructure:"Package" json:"package"` Package string `json:"package"`
Source string `mapstructure:"Source" json:"source" cyclonedx:"source"` Source string `json:"source" cyclonedx:"source"`
Version string `mapstructure:"Version" json:"version"` Version string `json:"version"`
SourceVersion string `mapstructure:"SourceVersion" json:"sourceVersion" cyclonedx:"sourceVersion"` SourceVersion string `json:"sourceVersion" cyclonedx:"sourceVersion"`
Architecture string `mapstructure:"Architecture" json:"architecture"`
Maintainer string `mapstructure:"Maintainer" json:"maintainer"` // Architecture can include the following sets of values depending on context and the control file used:
InstalledSize int `mapstructure:"InstalledSize" json:"installedSize" cyclonedx:"installedSize"` // - a unique single word identifying a Debian machine architecture as described in Architecture specification string (https://www.debian.org/doc/debian-policy/ch-customized-programs.html#s-arch-spec) .
Description string `mapstructure:"Description" hash:"ignore" json:"-"` // - an architecture wildcard identifying a set of Debian machine architectures, see Architecture wildcards (https://www.debian.org/doc/debian-policy/ch-customized-programs.html#s-arch-wildcard-spec). any matches all Debian machine architectures and is the most frequently used.
// - "all", which indicates an architecture-independent package.
// - "source", which indicates a source package.
Architecture string `json:"architecture"`
// Maintainer is the package maintainers name and email address. The name must come first, then the email
// address inside angle brackets <> (in RFC822 format).
Maintainer string `json:"maintainer"`
InstalledSize int `json:"installedSize" cyclonedx:"installedSize"`
// Description contains a description of the binary package, consisting of two parts, the synopsis or the short
// description, and the long description (in a multiline format).
Description string `hash:"ignore" json:"-"`
// Provides is a virtual package that is provided by one or more packages. A virtual package is one which appears
// in the Provides control field of another package. The effect is as if the package(s) which provide a particular
// virtual package name had been listed by name everywhere the virtual package name appears. (See also Virtual packages)
Provides []string `json:"provides,omitempty"`
// Depends This declares an absolute dependency. A package will not be configured unless all of the packages listed in
// its Depends field have been correctly configured (unless there is a circular dependency).
Depends []string `json:"depends,omitempty"`
// PreDepends is like Depends, except that it also forces dpkg to complete installation of the packages named
// before even starting the installation of the package which declares the pre-dependency.
PreDepends []string `json:"preDepends,omitempty"`
Files []DpkgFileRecord `json:"files"` Files []DpkgFileRecord `json:"files"`
} }