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
89 changes: 89 additions & 0 deletions binary/proto/package_vuln.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package proto

import (
"errors"
"fmt"

"github.com/google/osv-scalibr/extractor"
"github.com/google/osv-scalibr/inventory"
"github.com/google/osv-scalibr/inventory/vex"

spb "github.com/google/osv-scalibr/binary/proto/scan_result_go_proto"
)

var (
// ErrPackageMissing will be returned if the Package is not set on a PackageVuln.
ErrPackageMissing = errors.New("package is missing for PackageVuln")
)

// PackageVulnToProto converts a PackageVuln struct to proto.
func PackageVulnToProto(v *inventory.PackageVuln, pkgToID map[*extractor.Package]string) (*spb.PackageVuln, error) {
if v == nil {
return nil, nil
}

if v.Package == nil {
return nil, fmt.Errorf("package field is nil for PackageVuln %+v: %w", v, ErrPackageMissing)
}

pkgID, ok := pkgToID[v.Package]
if !ok {
return nil, fmt.Errorf("%v package %q version %q not found in pkgToID map", v.Package.Ecosystem().String(), v.Package.Name, v.Package.Version)
}

var exps []*spb.FindingExploitabilitySignal
for _, exp := range v.ExploitabilitySignals {
expProto := FindingVEXToProto(exp)
exps = append(exps, expProto)
}

return &spb.PackageVuln{
Vuln: v.Vulnerability,
PackageId: pkgID,
Plugins: v.Plugins,
ExploitabilitySignals: exps,
}, nil
}

// PackageVulnToStruct converts a PackageVuln proto into the equivalent go struct.
func PackageVulnToStruct(v *spb.PackageVuln, idToPkg map[string]*extractor.Package) (*inventory.PackageVuln, error) {
if v == nil {
return nil, nil
}

if v.GetPackageId() == "" {
return nil, fmt.Errorf("package ID is empty for PackageVuln %+v: %w", v, ErrPackageMissing)
}

pkg, ok := idToPkg[v.GetPackageId()]
if !ok {
return nil, fmt.Errorf("package with ID %q not found in idToPkg map", v.GetPackageId())
}

var exps []*vex.FindingExploitabilitySignal
for _, exp := range v.GetExploitabilitySignals() {
expStruct := FindingVEXToStruct(exp)
exps = append(exps, expStruct)
}

return &inventory.PackageVuln{
Vulnerability: v.GetVuln(),
Package: pkg,
Plugins: v.GetPlugins(),
ExploitabilitySignals: exps,
}, nil
}
176 changes: 176 additions & 0 deletions binary/proto/package_vuln_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package proto_test

import (
"errors"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cpy/cpy"
"github.com/google/osv-scalibr/binary/proto"
"github.com/google/osv-scalibr/extractor"
"github.com/google/osv-scalibr/inventory"
"github.com/google/osv-scalibr/inventory/vex"
"google.golang.org/protobuf/testing/protocmp"

spb "github.com/google/osv-scalibr/binary/proto/scan_result_go_proto"
osvpb "github.com/ossf/osv-schema/bindings/go/osvschema"
)

var (
idToPkg = map[string]*extractor.Package{
"1": purlDPKGAnnotationPackage,
}
pkgToID = func() map[*extractor.Package]string {
m := make(map[*extractor.Package]string)
for id, pkg := range idToPkg {
m[pkg] = id
}
return m
}()

pkgVulnStruct1 = &inventory.PackageVuln{
Vulnerability: &osvpb.Vulnerability{},
Package: purlDPKGAnnotationPackage,
Plugins: []string{"cve/cve-1234-finder", "cve/cve-1234-enricher"},
ExploitabilitySignals: []*vex.FindingExploitabilitySignal{{
Plugin: "some-plugin",
Justification: vex.ComponentNotPresent,
}},
}
pkgVulnProto1 = &spb.PackageVuln{
Vuln: &osvpb.Vulnerability{},
PackageId: "1",
Plugins: []string{"cve/cve-1234-finder", "cve/cve-1234-enricher"},
ExploitabilitySignals: []*spb.FindingExploitabilitySignal{
{
Plugin: "some-plugin",
Justification: spb.VexJustification_COMPONENT_NOT_PRESENT,
},
},
}
)

func TestPackageVulnSetup(t *testing.T) {
if len(pkgToID) != len(idToPkg) {
t.Fatalf("pkgToID and idToPkg have different lengths: %d != %d", len(pkgToID), len(idToPkg))
}
for pkg, id := range pkgToID {
otherPkg, ok := idToPkg[id]
if !ok {
t.Fatalf("package with ID %q not found in idToPkg map", id)
}
if pkg != otherPkg {
t.Fatalf("package with ID %q has different pointer value %v", id, otherPkg)
}
}
}

func TestPackageVulnToProto(t *testing.T) {
copier := cpy.New(
cpy.IgnoreAllUnexported(),
)

testCases := []struct {
desc string
pkgVuln *inventory.PackageVuln
want *spb.PackageVuln
wantErr error
}{
{
desc: "nil",
pkgVuln: nil,
want: nil,
},
{
desc: "success",
pkgVuln: pkgVulnStruct1,
want: pkgVulnProto1,
},
{
desc: "missing_package",
pkgVuln: func(p *inventory.PackageVuln) *inventory.PackageVuln {
p = copier.Copy(p).(*inventory.PackageVuln)
p.Package = nil
return p
}(pkgVulnStruct1),
want: nil,
wantErr: proto.ErrPackageMissing,
},
}

for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
got, err := proto.PackageVulnToProto(tc.pkgVuln, pkgToID)
if !errors.Is(err, tc.wantErr) {
t.Fatalf("PackageVulnToProto(%v, %v) returned error %v, want error %v", tc.pkgVuln, pkgToID, err, tc.wantErr)
}

if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
t.Fatalf("PackageVulnToProto(%+v, %+v) returned diff (-want +got):\n%s", tc.pkgVuln, pkgToID, diff)
}
})
}
}

func TestPackageVulnToStruct(t *testing.T) {
copier := cpy.New(
cpy.IgnoreAllUnexported(),
)

testCases := []struct {
desc string
pkgVuln *spb.PackageVuln
idToPkg map[string]*extractor.Package
want *inventory.PackageVuln
wantErr error
}{
{
desc: "nil",
pkgVuln: nil,
want: nil,
},
{
desc: "success",
pkgVuln: pkgVulnProto1,
idToPkg: idToPkg,
want: pkgVulnStruct1,
},
{
desc: "missing_package_ID",
pkgVuln: func(p *spb.PackageVuln) *spb.PackageVuln {
p = copier.Copy(p).(*spb.PackageVuln)
p.PackageId = ""
return p
}(pkgVulnProto1),
idToPkg: idToPkg,
want: nil,
wantErr: proto.ErrPackageMissing,
},
}

for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
got, err := proto.PackageVulnToStruct(tc.pkgVuln, tc.idToPkg)
if !errors.Is(err, tc.wantErr) {
t.Fatalf("PackageVulnToStruct(%v, %v) returned error %v, want error %v", tc.pkgVuln, tc.idToPkg, err, tc.wantErr)
}
if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
t.Fatalf("PackageVulnToStruct(%v, %v) returned diff (-want +got):\n%s", tc.pkgVuln, tc.idToPkg, diff)
}
})
}
}
14 changes: 14 additions & 0 deletions binary/proto/scan_result.proto
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ syntax = "proto3";
package scalibr;

import "google/protobuf/timestamp.proto";
import "proto/vulnerability.proto";

option go_package = "github.com/google/osv-scalibr/binary/proto/scan_result_go_proto";
option java_multiple_files = true;
Expand All @@ -43,6 +44,7 @@ message ScanResult {
// The artifacts (e.g. software inventory, security findings) that a scan found.
message Inventory {
repeated Package packages = 1;
repeated PackageVuln package_vulns = 6;
repeated GenericFinding generic_findings = 2;
repeated Secret secrets = 3;
repeated ContainerImageMetadata container_image_metadata = 5;
Expand Down Expand Up @@ -248,6 +250,18 @@ message Qualifier {
string value = 2;
}

// Describes a vulnerability (e.g. a CVE) related to a package.
message PackageVuln {
osv.Vulnerability vuln = 1;
// The ID of the associated package in Inventory.Packages.
// Used for mapping between proto and struct.
string package_id = 2;
// The plugins (e.g. Detectors, Enrichers) that found this vuln.
repeated string plugins = 3;
// Signals that indicate this finding is not exploitable.
repeated FindingExploitabilitySignal exploitability_signals = 4;
}

// Describes generic security findings not associated with any
// specific package, e.g. weak credentials.
message GenericFinding {
Expand Down
Loading
Loading