Skip to content
162 changes: 162 additions & 0 deletions pkg/lockfile/dpkg-status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package lockfile

import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
)

const DebianEcosystem Ecosystem = "Debian"

func groupDpkgPackageLines(scanner *bufio.Scanner) [][]string {
Comment thread
cmaritan marked this conversation as resolved.
var groups [][]string
var group []string

for scanner.Scan() {
line := scanner.Text()

if line != "" {
group = append(group, line)
continue
}
if len(group) > 0 {
groups = append(groups, group)
}
group = make([]string, 0)
}

if len(group) > 0 {
groups = append(groups, group)
}

return groups
}

// Return name and version if "Source" field contains them
func parseSourceField(source string) (string, string) {
// Pattern: name (version)
re := regexp.MustCompile(`^(.*)\((.*)\)`)
matches := re.FindStringSubmatch(source)
if len(matches) == 3 {
return strings.TrimSpace(matches[1]), strings.TrimSpace(matches[2])
}
// If it not matches the pattern "name (version)", it is only "name"
return strings.TrimSpace(source), ""
}

func parseDpkgPackageGroup(group []string, pathToLockfile string) PackageDetails {
var pkg = PackageDetails{
Ecosystem: DebianEcosystem,
CompareAs: DebianEcosystem,
}

sourcePresent := false
sourceHasVersion := false
for _, line := range group {
switch {
// Status field SPECS: http://www.fifi.org/doc/libapt-pkg-doc/dpkg-tech.html/ch1.html#s1.2
case strings.HasPrefix(line, "Status:"):
status := strings.TrimPrefix(line, "Status:")
tokens := strings.Fields(status)
// Staus field is malformed. Expected: "Status: Want Flag Status"
if len(tokens) != 3 {
_, _ = fmt.Fprintf(
os.Stderr,
"warning: malformed DPKG status file. Found no valid \"Source\" field. File: %s\n",
pathToLockfile,
)

return PackageDetails{}
}
// Status field has correct number of fields but package is not installed or has only config files left
// various other field values indicate partial install/uninstall (e.g. failure of some pre/post install scripts)
// since it's not clear if failure has left package active on system, cautiously add it to queries to osv.dev
if tokens[2] == "not-installed" || tokens[2] == "config-files" {
return PackageDetails{}
}

case strings.HasPrefix(line, "Source:"):
sourcePresent = true
source := strings.TrimPrefix(line, "Source:")
name, version := parseSourceField(source)
pkg.Name = name // can be ""
if version != "" {
sourceHasVersion = true
pkg.Version = version
}

// If Source field has no version, use Version field
case strings.HasPrefix(line, "Version:"):
if !sourceHasVersion {
pkg.Version = strings.TrimPrefix(line, "Version:")
pkg.Version = strings.TrimSpace(pkg.Version)
}

// Some packages have no Source field (e.g. sudo) so we use Package value
case strings.HasPrefix(line, "Package:"):
if !sourcePresent {
pkg.Name = strings.TrimPrefix(line, "Package:")
pkg.Name = strings.TrimSpace(pkg.Name)
}
}
}

if pkg.Version == "" {
pkgPrintName := pkg.Name
if pkgPrintName == "" {
pkgPrintName = unknownPkgName
}

_, _ = fmt.Fprintf(
os.Stderr,
"warning: malformed DPKG status file. Found no version number in record. Package %s. File: %s\n",
pkgPrintName,
pathToLockfile,
)
}

return pkg
}

func ParseDpkgStatus(pathToLockfile string) ([]PackageDetails, error) {
file, err := os.Open(pathToLockfile)
if err != nil {
return []PackageDetails{}, fmt.Errorf("could not open %s: %w", pathToLockfile, err)
}
defer file.Close()

scanner := bufio.NewScanner(file)
packageGroups := groupDpkgPackageLines(scanner)

packages := make([]PackageDetails, 0, len(packageGroups))

for _, group := range packageGroups {
pkg := parseDpkgPackageGroup(group, pathToLockfile)

// PackageDetails does not contain any field that represent a "not installed" state
// To manage this state and avoid false positives, empty struct means "not installed" so skip it
if (PackageDetails{}) == pkg {
continue
}

if pkg.Name == "" {
_, _ = fmt.Fprintf(
os.Stderr,
"warning: malformed DPKG status file. Found no package name in record. File: %s\n",
pathToLockfile,
)

continue
}

packages = append(packages, pkg)
}

if err := scanner.Err(); err != nil {
return packages, fmt.Errorf("error while scanning %s: %w", pathToLockfile, err)
}

return packages, nil
}
153 changes: 153 additions & 0 deletions pkg/lockfile/dpkg-status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package lockfile_test

import (
"testing"

"github.com/google/osv-scanner/pkg/lockfile"
)

func TestDpkgStatus_FileDoesNotExist(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseDpkgStatus("fixtures/dpkg/does-not-exist")

expectErrContaining(t, err, "could not open")
expectPackages(t, packages, []lockfile.PackageDetails{})
}

func TestDpkgStatus_Empty(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseDpkgStatus("fixtures/dpkg/empty_status")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{})
}

func TestDpkgStatus_NotAStatus(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseDpkgStatus("fixtures/dpkg/not_status")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{})
}

func TestDpkgStatus_Malformed(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseDpkgStatus("fixtures/dpkg/malformed_status")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "bash",
Version: "",
Ecosystem: lockfile.DebianEcosystem,
CompareAs: lockfile.DebianEcosystem,
},
{
Name: "util-linux",
Version: "2.36.1-8+deb11u1",
Ecosystem: lockfile.DebianEcosystem,
CompareAs: lockfile.DebianEcosystem,
},
})
}

func TestDpkgStatus_Single(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseDpkgStatus("fixtures/dpkg/single_status")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "sudo",
Version: "1.8.27-1+deb10u1",
Ecosystem: lockfile.DebianEcosystem,
CompareAs: lockfile.DebianEcosystem,
},
})
}

func TestDpkgStatus_Shuffled(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseDpkgStatus("fixtures/dpkg/shuffled_status")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "glibc",
Version: "2.31-13+deb11u5",
Ecosystem: lockfile.DebianEcosystem,
CompareAs: lockfile.DebianEcosystem,
},
})
}

func TestDpkgStatus_Multiple(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseDpkgStatus("fixtures/dpkg/multiple_status")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "bash",
Version: "5.1-2+deb11u1",
Ecosystem: lockfile.DebianEcosystem,
CompareAs: lockfile.DebianEcosystem,
},
{
Name: "util-linux",
Version: "2.36.1-8+deb11u1",
Ecosystem: lockfile.DebianEcosystem,
CompareAs: lockfile.DebianEcosystem,
},
{
Name: "glibc",
Version: "2.31-13+deb11u5",
Ecosystem: lockfile.DebianEcosystem,
CompareAs: lockfile.DebianEcosystem,
},
})
}

func TestDpkgStatus_Source_Ver_Override(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseDpkgStatus("fixtures/dpkg/source_ver_override_status")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "lvm2",
Version: "2.02.176-4.1ubuntu3",
Ecosystem: lockfile.DebianEcosystem,
CompareAs: lockfile.DebianEcosystem,
},
})
}
Empty file.
Loading