Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ These models are defined in Python ([`osv/models.py`](osv/models.py)) and mirror
* **Rule**: This entity is **only** used by the website and should not be used for API matching logic.

5. **`UbuntuPackageMapping` (Kind: `UbuntuPackageMapping`)**
* **Purpose**: Used by the experimental `POST /v1experimental/ubuntu/binary-to-source` API endpoint to resolve Ubuntu binary package names to their corresponding source package names.
* **Fields**: Keyed by binary package name; stores `source_names` (unindexed list of corresponding Ubuntu source package names).
* **Purpose**: Used by the experimental `POST /v1experimental/ubuntu/binary-to-source` API endpoint to resolve Ubuntu binary package names to their corresponding source package names within an Ubuntu release ecosystem.
* **Fields**: Keyed by `<normalized_ecosystem>:<binary_name>` (e.g., `Ubuntu:24.04:libcurl4`); stores `source_names` (unindexed list of corresponding Ubuntu source package names).
* **Note**: Defined in Go ([`go/internal/database/datastore/ubuntu_package_mapping.go`](go/internal/database/datastore/ubuntu_package_mapping.go)) and populated by `ubuntubinarymapper`.

---
Expand Down
13 changes: 11 additions & 2 deletions bindings/go/api/osv_service_v1.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions docs/api/post-v1-ubuntu-binary-to-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ package names.
## Parameters

|---
| Parameter | Type | Description |
| -------------- | ----- | ----------------------------------------------------------------------------- |
| `binary_names` | array | A non-empty array of Ubuntu binary package names (maximum 1000 per request). |
| Parameter | Type | Description |
| -------------- | ------ | -------------------------------------------------------------------------------------------------------------------- |
| `ecosystem` | string | The Ubuntu release ecosystem (e.g. `Ubuntu:24.04`, `Ubuntu:24.04:LTS`, or `Ubuntu:Pro:24.04:LTS`). |
| `binary_names` | array | A non-empty array of Ubuntu binary package names (maximum 1000 per request). |

## Payload

```json
{
"ecosystem": "string",
"binary_names": [
"string"
]
Expand All @@ -59,6 +61,7 @@ package names.
```bash
cat <<EOF | curl -d @- "https://api.osv.dev/v1experimental/ubuntu/binary-to-source"
{
"ecosystem": "Ubuntu:24.04:LTS",
"binary_names": [
"libcurl4",
"libglib2.0-0"
Expand All @@ -71,7 +74,7 @@ EOF

The response `results` array is guaranteed to match the ordering of the input
`binary_names` (with an empty object `{}` for binary package names that have no
known source package mappings):
known source package mappings in that Ubuntu release):

```json
{
Expand Down
3 changes: 3 additions & 0 deletions docs/osv_service_v1.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,9 @@
"items": {
"type": "string"
}
},
"ecosystem": {
"type": "string"
}
},
"description": "Parameters for QueryUbuntuPackageMapping."
Expand Down
62 changes: 37 additions & 25 deletions go/cmd/ubuntubinarymapper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,15 @@ func findModifiedUbuntuIDs(ctx context.Context, gcsStorage clients.CloudStorage,
return ids, nil
}

// ExtractBinaryMappings extracts a map of binary_name -> slice of source_names from a Vulnerability record.
func ExtractBinaryMappings(vuln *osvschema.Vulnerability) map[string][]string {
mappings := make(map[string][]string)
// ExtractBinaryMappings extracts a map of (ecosystem, binary_name) -> slice of source_names from a Vulnerability record.
func ExtractBinaryMappings(vuln *osvschema.Vulnerability) map[models.UbuntuPackageKey][]string {
mappings := make(map[models.UbuntuPackageKey][]string)
for _, affected := range vuln.GetAffected() {
if !strings.HasPrefix(affected.GetPackage().GetEcosystem(), "Ubuntu") {
rawEco := affected.GetPackage().GetEcosystem()
if !models.IsValidUbuntuReleaseEcosystem(rawEco) {
continue
}
eco := models.NormalizeUbuntuEcosystem(rawEco)
sourceName := affected.GetPackage().GetName()
if sourceName == "" {
continue
Expand All @@ -144,10 +146,17 @@ func ExtractBinaryMappings(vuln *osvschema.Vulnerability) map[string][]string {
binaries := affected.GetEcosystemSpecific().GetFields()["binaries"].GetListValue().GetValues()
for _, item := range binaries {
binName := strings.TrimSpace(item.GetStructValue().GetFields()["binary_name"].GetStringValue())
if binName == "" {
continue
}
key := models.UbuntuPackageKey{
Ecosystem: eco,
BinaryName: binName,
}
// A binary typically maps to only 1-2 unique source packages, so slices.Contains
// avoids duplicate appends across releases without per-binary map overhead.
if binName != "" && !slices.Contains(mappings[binName], sourceName) {
mappings[binName] = append(mappings[binName], sourceName)
// avoids duplicate appends without per-binary map overhead.
if !slices.Contains(mappings[key], sourceName) {
mappings[key] = append(mappings[key], sourceName)
}
}
}
Expand Down Expand Up @@ -260,7 +269,7 @@ func run(ctx context.Context, env *appEnv) error {
if len(vulnIDs) > 0 {
threshold := cmp.Or(env.zipThreshold, zipDownloadThreshold)
workers := max(cmp.Or(env.numWorkers, defaultNumWorkers), 1)
var allMappings map[string][]string
var allMappings map[models.UbuntuPackageKey][]string
if len(vulnIDs) > threshold {
logger.InfoContext(ctx, "downloading Ubuntu/all.zip for bulk processing", slog.Int("count", len(vulnIDs)), slog.Int("threshold", threshold))
allMappings, err = extractMappingsFromAllZip(ctx, env.gcsStorage, vulnIDs, workers)
Expand All @@ -272,7 +281,7 @@ func run(ctx context.Context, env *appEnv) error {
return err
}

logger.InfoContext(ctx, "extracted binary package mappings", slog.Int("unique_binaries", len(allMappings)))
logger.InfoContext(ctx, "extracted binary package mappings", slog.Int("unique_keys", len(allMappings)))
if len(allMappings) > 0 {
if err := saveMappings(ctx, env.ubuntuStore, allMappings); err != nil {
return fmt.Errorf("failed saving mappings: %w", err)
Expand All @@ -289,7 +298,7 @@ func run(ctx context.Context, env *appEnv) error {
return nil
}

func extractMappingsFromAllZip(ctx context.Context, gcsStorage clients.CloudStorage, vulnIDs []string, numWorkers int) (map[string][]string, error) {
func extractMappingsFromAllZip(ctx context.Context, gcsStorage clients.CloudStorage, vulnIDs []string, numWorkers int) (map[models.UbuntuPackageKey][]string, error) {
zipBytes, err := gcsStorage.ReadObject(ctx, ubuntuAllZipPath)
if err != nil {
return nil, fmt.Errorf("failed reading %s: %w", ubuntuAllZipPath, err)
Expand All @@ -306,7 +315,7 @@ func extractMappingsFromAllZip(ctx context.Context, gcsStorage clients.CloudStor
}

var mu sync.Mutex
allMappings := make(map[string][]string)
allMappings := make(map[models.UbuntuPackageKey][]string)

g, gCtx := errgroup.WithContext(ctx)
g.SetLimit(numWorkers)
Expand Down Expand Up @@ -345,9 +354,9 @@ func extractMappingsFromAllZip(ctx context.Context, gcsStorage clients.CloudStor
return allMappings, nil
}

func extractMappingsFromIndividualFiles(ctx context.Context, gcsStorage clients.CloudStorage, vulnIDs []string, numWorkers int) (map[string][]string, error) {
func extractMappingsFromIndividualFiles(ctx context.Context, gcsStorage clients.CloudStorage, vulnIDs []string, numWorkers int) (map[models.UbuntuPackageKey][]string, error) {
var mu sync.Mutex
allMappings := make(map[string][]string)
allMappings := make(map[models.UbuntuPackageKey][]string)

g, ctx := errgroup.WithContext(ctx)
g.SetLimit(numWorkers)
Expand Down Expand Up @@ -377,7 +386,7 @@ func extractMappingsFromIndividualFiles(ctx context.Context, gcsStorage clients.
return allMappings, nil
}

func unmarshalAndMerge(data []byte, source string, mu *sync.Mutex, allMappings map[string][]string) error {
func unmarshalAndMerge(data []byte, source string, mu *sync.Mutex, allMappings map[models.UbuntuPackageKey][]string) error {
var vuln osvschema.Vulnerability
if err := (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(data, &vuln); err != nil {
return fmt.Errorf("failed unmarshaling %s: %w", source, err)
Expand All @@ -390,39 +399,42 @@ func unmarshalAndMerge(data []byte, source string, mu *sync.Mutex, allMappings m

mu.Lock()
defer mu.Unlock()
for bin, sources := range recordMappings {
for key, sources := range recordMappings {
for _, src := range sources {
// A binary only has 1-2 source packages, so slices.Contains keeps the slice bounded
// across thousands of records without allocating nested maps.
if !slices.Contains(allMappings[bin], src) {
allMappings[bin] = append(allMappings[bin], src)
if !slices.Contains(allMappings[key], src) {
allMappings[key] = append(allMappings[key], src)
}
}
}

return nil
}

func saveMappings(ctx context.Context, store models.UbuntuPackageMappingStore, newMappings map[string][]string) error {
binaryNames := slices.Sorted(maps.Keys(newMappings))
func saveMappings(ctx context.Context, store models.UbuntuPackageMappingStore, newMappings map[models.UbuntuPackageKey][]string) error {
keys := slices.SortedFunc(maps.Keys(newMappings), func(a, b models.UbuntuPackageKey) int {
return cmp.Or(cmp.Compare(a.Ecosystem, b.Ecosystem), cmp.Compare(a.BinaryName, b.BinaryName))
})

// GetMulti returns a 1:1 slice matching binaryNames.
existing, err := store.GetMulti(ctx, binaryNames)
// GetMulti returns a 1:1 slice matching keys.
existing, err := store.GetMulti(ctx, keys)
if err != nil {
return fmt.Errorf("failed getting existing mappings: %w", err)
}

toPut := make([]*models.UbuntuPackageMapping, len(binaryNames))
for i, bin := range binaryNames {
toPut := make([]*models.UbuntuPackageMapping, len(keys))
for i, key := range keys {
var existingSources []string
if i < len(existing) && existing[i] != nil {
existingSources = existing[i].SourceNames
}
merged := slices.Concat(existingSources, newMappings[bin])
merged := slices.Concat(existingSources, newMappings[key])
slices.Sort(merged)

toPut[i] = &models.UbuntuPackageMapping{
BinaryName: bin,
Ecosystem: key.Ecosystem,
BinaryName: key.BinaryName,
SourceNames: slices.Compact(merged),
}
}
Expand Down
40 changes: 28 additions & 12 deletions go/cmd/ubuntubinarymapper/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestExtractBinaryMappings(t *testing.T) {
{
Package: &osvschema.Package{
Name: "glib2.0",
Ecosystem: "Ubuntu:24.04:LTS",
Ecosystem: "Ubuntu:Pro:24.04:LTS",
},
EcosystemSpecific: ecoStruct,
},
Expand All @@ -70,6 +70,13 @@ func TestExtractBinaryMappings(t *testing.T) {
Ecosystem: "Ubuntu:22.04:LTS",
},
},
{
Package: &osvschema.Package{
Name: "bare-ubuntu-pkg",
Ecosystem: "Ubuntu",
},
EcosystemSpecific: ecoStruct,
},
{
Package: &osvschema.Package{
Name: "debian-pkg",
Expand All @@ -82,9 +89,11 @@ func TestExtractBinaryMappings(t *testing.T) {

got := ExtractBinaryMappings(vuln)

expected := map[string][]string{
"libglib2.0-0": {"glib2.0"},
"libglib2.0-bin": {"glib2.0"},
expected := map[models.UbuntuPackageKey][]string{
{Ecosystem: "Ubuntu:22.04", BinaryName: "libglib2.0-0"}: {"glib2.0"},
{Ecosystem: "Ubuntu:22.04", BinaryName: "libglib2.0-bin"}: {"glib2.0"},
{Ecosystem: "Ubuntu:24.04", BinaryName: "libglib2.0-0"}: {"glib2.0"},
{Ecosystem: "Ubuntu:24.04", BinaryName: "libglib2.0-bin"}: {"glib2.0"},
}

if diff := cmp.Diff(expected, got); diff != "" {
Expand Down Expand Up @@ -199,7 +208,7 @@ func TestRun_EndToEnd(t *testing.T) {
Id: "USN-2-1",
Affected: []*osvschema.Affected{
{
Package: &osvschema.Package{Name: "curl-esm-src", Ecosystem: "Ubuntu:22.04:LTS"},
Package: &osvschema.Package{Name: "curl-esm-src", Ecosystem: "Ubuntu:Pro:22.04:LTS"},
EcosystemSpecific: ecoStruct2,
},
},
Expand Down Expand Up @@ -238,13 +247,16 @@ func TestRun_EndToEnd(t *testing.T) {
t.Fatalf("initial run failed: %v", err)
}

got, err := ubuntuStore.GetMulti(ctx, []string{"curl", "libcurl4"})
got, err := ubuntuStore.GetMulti(ctx, []models.UbuntuPackageKey{
{Ecosystem: "Ubuntu:22.04", BinaryName: "curl"},
{Ecosystem: "Ubuntu:22.04:LTS", BinaryName: "libcurl4"},
})
if err != nil {
t.Fatalf("GetMulti failed: %v", err)
}
wantInitial := []*models.UbuntuPackageMapping{
{BinaryName: "curl", SourceNames: []string{"curl"}},
{BinaryName: "libcurl4", SourceNames: []string{"curl"}},
{Ecosystem: "Ubuntu:22.04", BinaryName: "curl", SourceNames: []string{"curl"}},
{Ecosystem: "Ubuntu:22.04", BinaryName: "libcurl4", SourceNames: []string{"curl"}},
}
if diff := cmp.Diff(wantInitial, got); diff != "" {
t.Errorf("initial mappings mismatch (-want +got):\n%s", diff)
Expand All @@ -262,14 +274,18 @@ func TestRun_EndToEnd(t *testing.T) {
t.Fatalf("incremental run failed: %v", err)
}

gotAfter, err := ubuntuStore.GetMulti(ctx, []string{"curl", "libcurl4", "curl-esm"})
gotAfter, err := ubuntuStore.GetMulti(ctx, []models.UbuntuPackageKey{
{Ecosystem: "Ubuntu:22.04", BinaryName: "curl"},
{Ecosystem: "Ubuntu:22.04", BinaryName: "libcurl4"},
{Ecosystem: "Ubuntu:22.04", BinaryName: "curl-esm"},
})
if err != nil {
t.Fatalf("GetMulti after incremental run failed: %v", err)
}
wantAfter := []*models.UbuntuPackageMapping{
{BinaryName: "curl", SourceNames: []string{"curl"}},
{BinaryName: "libcurl4", SourceNames: []string{"curl", "curl-esm-src"}},
{BinaryName: "curl-esm", SourceNames: []string{"curl-esm-src"}},
{Ecosystem: "Ubuntu:22.04", BinaryName: "curl", SourceNames: []string{"curl"}},
{Ecosystem: "Ubuntu:22.04", BinaryName: "libcurl4", SourceNames: []string{"curl", "curl-esm-src"}},
{Ecosystem: "Ubuntu:22.04", BinaryName: "curl-esm", SourceNames: []string{"curl-esm-src"}},
}
if diff := cmp.Diff(wantAfter, gotAfter); diff != "" {
t.Errorf("mappings mismatch after incremental merge (-want +got):\n%s", diff)
Expand Down
24 changes: 21 additions & 3 deletions go/internal/api/ubuntu_package_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"log/slog"

"github.com/google/osv.dev/go/internal/models"
"github.com/google/osv.dev/go/logger"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
Expand All @@ -27,6 +28,7 @@ import (
const (
maxBinaryNames = 1000
maxBinaryNameLen = 256
maxEcosystemLen = 256
)

// QueryUbuntuPackageMapping handles querying Ubuntu binary package names to retrieve their corresponding source package names.
Expand All @@ -35,6 +37,15 @@ func (s *server) QueryUbuntuPackageMapping(ctx context.Context, params *pb.Ubunt
return nil, status.Error(codes.Internal, "ubuntu package mapping store is not configured")
}

rawEcosystem := params.GetEcosystem()
if rawEcosystem == "" {
return nil, status.Error(codes.InvalidArgument, "ecosystem is required")
}
if len(rawEcosystem) > maxEcosystemLen || !models.IsValidUbuntuReleaseEcosystem(rawEcosystem) {
return nil, status.Error(codes.InvalidArgument, "invalid ubuntu ecosystem: must include a release suffix (e.g. Ubuntu:22.04)")
}
ecosystem := models.NormalizeUbuntuEcosystem(rawEcosystem)

binaryNames := params.GetBinaryNames()
if len(binaryNames) == 0 {
return nil, status.Error(codes.InvalidArgument, "binary_names is required")
Expand All @@ -43,18 +54,25 @@ func (s *server) QueryUbuntuPackageMapping(ctx context.Context, params *pb.Ubunt
return nil, status.Errorf(codes.InvalidArgument, "too many binary_names (max %d)", maxBinaryNames)
}

keys := make([]models.UbuntuPackageKey, len(binaryNames))
for i, name := range binaryNames {
if name == "" || len(name) > maxBinaryNameLen {
return nil, status.Errorf(codes.InvalidArgument, "invalid binary package name at index %d", i)
}
keys[i] = models.UbuntuPackageKey{
Ecosystem: ecosystem,
BinaryName: name,
}
}

if s.verboseLogs {
logger.InfoContext(ctx, "querying ubuntu package mapping", slog.Any("binary_names", binaryNames))
logger.InfoContext(ctx, "querying ubuntu package mapping",
slog.String("ecosystem", ecosystem),
slog.Any("binary_names", binaryNames))
}

// GetMulti returns a 1:1 slice matching binaryNames.
mappings, err := s.ubuntuPackageMappingStore.GetMulti(ctx, binaryNames)
// GetMulti returns a 1:1 slice matching keys.
mappings, err := s.ubuntuPackageMappingStore.GetMulti(ctx, keys)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get ubuntu package mappings: %v", err)
}
Expand Down
Loading
Loading