Skip to content

Commit 2fe8d23

Browse files
erikvargacopybara-github
authored andcommitted
Add Annotator plugin type and move cachedir into an Annotator.
PiperOrigin-RevId: 760648867
1 parent e748fbb commit 2fe8d23

File tree

12 files changed

+532
-93
lines changed

12 files changed

+532
-93
lines changed

annotator/annotator.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package annotator provides the interface for annotation plugins.
16+
package annotator
17+
18+
import (
19+
"context"
20+
21+
scalibrfs "github.com/google/osv-scalibr/fs"
22+
"github.com/google/osv-scalibr/inventory"
23+
"github.com/google/osv-scalibr/plugin"
24+
)
25+
26+
// Annotator is the interface for an annotation plugin, used to add additional
27+
// information to scan results such as VEX statements. Annotators have access to
28+
// the filesystem but should ideally not query any external APIs. If you need to
29+
// modify the scan results based on the output of network calls you should use
30+
// the Enricher interface instead.
31+
type Annotator interface {
32+
plugin.Plugin
33+
// Annotate annotates the scan results with additional information.
34+
Annotate(ctx context.Context, input *ScanInput, results *inventory.Inventory) error
35+
}
36+
37+
// Config stores the config settings for the annotation run.
38+
type Config struct {
39+
Annotators []Annotator
40+
ScanRoot *scalibrfs.ScanRoot
41+
}
42+
43+
// ScanInput provides information for the annotator about the scan.
44+
type ScanInput struct {
45+
// The root of the artifact being scanned.
46+
ScanRoot *scalibrfs.ScanRoot
47+
}
48+
49+
// Run runs the specified annotators on the scan results and returns their statuses.
50+
func Run(ctx context.Context, config *Config, inventory *inventory.Inventory) ([]*plugin.Status, error) {
51+
var statuses []*plugin.Status
52+
if len(config.Annotators) == 0 {
53+
return statuses, nil
54+
}
55+
56+
input := &ScanInput{
57+
ScanRoot: config.ScanRoot,
58+
}
59+
60+
for _, a := range config.Annotators {
61+
err := a.Annotate(ctx, input, inventory)
62+
statuses = append(statuses, plugin.StatusFromErr(a, false, err))
63+
}
64+
return statuses, nil
65+
}

annotator/annotator_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package annotator_test
16+
17+
import (
18+
"context"
19+
"errors"
20+
"testing"
21+
22+
"github.com/google/go-cmp/cmp"
23+
"github.com/google/go-cmp/cmp/cmpopts"
24+
"github.com/google/go-cpy/cpy"
25+
"github.com/google/osv-scalibr/annotator"
26+
"github.com/google/osv-scalibr/annotator/cachedir"
27+
"github.com/google/osv-scalibr/extractor"
28+
"github.com/google/osv-scalibr/inventory"
29+
"github.com/google/osv-scalibr/plugin"
30+
"google.golang.org/protobuf/proto"
31+
)
32+
33+
type succeedingAnnotator struct{}
34+
35+
func (succeedingAnnotator) Name() string { return "succeeding-annotator" }
36+
func (succeedingAnnotator) Version() int { return 1 }
37+
func (succeedingAnnotator) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} }
38+
func (succeedingAnnotator) Annotate(ctx context.Context, input *annotator.ScanInput, results *inventory.Inventory) error {
39+
return nil
40+
}
41+
42+
type failingAnnotator struct{}
43+
44+
func (failingAnnotator) Name() string { return "failing-annotator" }
45+
func (failingAnnotator) Version() int { return 2 }
46+
func (failingAnnotator) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} }
47+
func (failingAnnotator) Annotate(ctx context.Context, input *annotator.ScanInput, results *inventory.Inventory) error {
48+
return errors.New("some error")
49+
}
50+
51+
func TestRun(t *testing.T) {
52+
inv := &inventory.Inventory{
53+
Packages: []*extractor.Package{
54+
{Name: "package1", Version: "1.0", Locations: []string{"tmp/package.json"}},
55+
},
56+
}
57+
58+
copier := cpy.New(
59+
cpy.Func(proto.Clone),
60+
cpy.IgnoreAllUnexported(),
61+
)
62+
63+
tests := []struct {
64+
desc string
65+
cfg *annotator.Config
66+
inv *inventory.Inventory
67+
want []*plugin.Status
68+
wantErr error
69+
wantInv *inventory.Inventory // Inventory after annotation.
70+
}{
71+
{
72+
desc: "no_annotators",
73+
cfg: &annotator.Config{},
74+
want: nil,
75+
},
76+
{
77+
desc: "annotator_modifies_inventory",
78+
cfg: &annotator.Config{
79+
Annotators: []annotator.Annotator{cachedir.New()},
80+
},
81+
inv: inv,
82+
want: []*plugin.Status{
83+
{Name: "vex/cachedir", Version: 0, Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded}},
84+
},
85+
wantInv: &inventory.Inventory{
86+
Packages: []*extractor.Package{
87+
{
88+
Name: "package1",
89+
Version: "1.0",
90+
Locations: []string{"tmp/package.json"},
91+
Annotations: []extractor.Annotation{extractor.InsideCacheDir},
92+
},
93+
},
94+
},
95+
},
96+
{
97+
desc: "annotator_fails",
98+
cfg: &annotator.Config{
99+
Annotators: []annotator.Annotator{&failingAnnotator{}},
100+
},
101+
want: []*plugin.Status{
102+
{Name: "failing-annotator", Version: 2, Status: &plugin.ScanStatus{Status: plugin.ScanStatusFailed, FailureReason: "some error"}},
103+
},
104+
},
105+
{
106+
desc: "one_fails_one_succeeds",
107+
cfg: &annotator.Config{
108+
Annotators: []annotator.Annotator{&succeedingAnnotator{}, &failingAnnotator{}},
109+
},
110+
want: []*plugin.Status{
111+
{Name: "succeeding-annotator", Version: 1, Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded}},
112+
{Name: "failing-annotator", Version: 2, Status: &plugin.ScanStatus{Status: plugin.ScanStatusFailed, FailureReason: "some error"}},
113+
},
114+
},
115+
}
116+
117+
for _, tc := range tests {
118+
t.Run(tc.desc, func(t *testing.T) {
119+
// Deep copy the inventory to avoid modifying the original inventory that is used in other tests.
120+
inv := copier.Copy(tc.inv).(*inventory.Inventory)
121+
got, err := annotator.Run(context.Background(), tc.cfg, inv)
122+
if !cmp.Equal(err, tc.wantErr, cmpopts.EquateErrors()) {
123+
t.Errorf("Run(%+v) error: got %v, want %v\n", tc.cfg, err, tc.wantErr)
124+
}
125+
if diff := cmp.Diff(tc.want, got); diff != "" {
126+
t.Errorf("Run(%+v) returned an unexpected diff of statuses (-want +got): %v", tc.cfg, diff)
127+
}
128+
if diff := cmp.Diff(tc.wantInv, inv); diff != "" {
129+
t.Errorf("Run(%+v) returned an unexpected diff of mutated inventory (-want +got): %v", tc.cfg, diff)
130+
}
131+
})
132+
}
133+
}

extractor/annotator/cachedir.go renamed to annotator/cachedir/cachedir.go

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,23 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package annotator
15+
// Package cachedir implements an annotator for packages that are in cache directories.
16+
package cachedir
1617

1718
import (
19+
"context"
1820
"path/filepath"
1921
"regexp"
22+
23+
"github.com/google/osv-scalibr/annotator"
24+
"github.com/google/osv-scalibr/extractor"
25+
"github.com/google/osv-scalibr/inventory"
26+
"github.com/google/osv-scalibr/plugin"
27+
)
28+
29+
const (
30+
// Name of the Annotator.
31+
Name = "vex/cachedir"
2032
)
2133

2234
// patterns to match cache directories
@@ -38,12 +50,41 @@ var cacheDirPatterns = []*regexp.Regexp{
3850
regexp.MustCompile(`(C:/)?Windows/Temp/`),
3951
}
4052

41-
// IsInsideCacheDir checks if the given path is inside a cache directory.
42-
func IsInsideCacheDir(path string) bool {
53+
// Annotator adds annotations to packages that are in cache directories.
54+
type Annotator struct{}
55+
56+
// New returns a new Annotator.
57+
func New() annotator.Annotator { return &Annotator{} }
58+
59+
// Name of the annotator.
60+
func (Annotator) Name() string { return Name }
61+
62+
// Version of the annotator.
63+
func (Annotator) Version() int { return 0 }
64+
65+
// Requirements of the annotator.
66+
func (Annotator) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} }
67+
68+
// Annotate adds annotations to packages that are in cache directories.
69+
func (Annotator) Annotate(ctx context.Context, input *annotator.ScanInput, results *inventory.Inventory) error {
70+
for _, pkg := range results.Packages {
71+
if ctx.Err() != nil {
72+
return ctx.Err()
73+
}
74+
for _, loc := range pkg.Locations {
75+
if isInsideCacheDir(loc) {
76+
pkg.Annotations = append(pkg.Annotations, extractor.InsideCacheDir)
77+
break
78+
}
79+
}
80+
}
81+
return nil
82+
}
83+
84+
func isInsideCacheDir(path string) bool {
4385
path = filepath.ToSlash(path)
4486

4587
// Check if the absolute path matches any of the known cache directory patterns
46-
4788
for _, r := range cacheDirPatterns {
4889
if r.MatchString(path) {
4990
return true

annotator/cachedir/cachedir_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cachedir_test
16+
17+
import (
18+
"context"
19+
"os"
20+
"testing"
21+
22+
"github.com/google/go-cmp/cmp"
23+
"github.com/google/osv-scalibr/annotator/cachedir"
24+
"github.com/google/osv-scalibr/extractor"
25+
"github.com/google/osv-scalibr/inventory"
26+
)
27+
28+
func TestIsInsideCacheDir(t *testing.T) {
29+
// Define test cases with different platform-specific paths
30+
testCases := []struct {
31+
inputPath string
32+
separator rune // defaulting to '/'
33+
wantCacheAnnotation bool
34+
}{
35+
// Linux/Unix
36+
{inputPath: "/tmp/somefile", wantCacheAnnotation: true},
37+
{inputPath: "/var/cache/apt/archives", wantCacheAnnotation: true},
38+
{inputPath: "/home/user/.local/share/Trash/files/file.txt", wantCacheAnnotation: true},
39+
{inputPath: "/home/user/.cache/thumbnails", wantCacheAnnotation: true},
40+
{inputPath: "/home/user/projects/code", wantCacheAnnotation: false},
41+
42+
// macOS
43+
{inputPath: "/Users/username/Library/Caches/com.apple.Safari", wantCacheAnnotation: true},
44+
{inputPath: "/private/tmp/mytmpfile", wantCacheAnnotation: true},
45+
{inputPath: "/System/Volumes/Data/private/var/tmp/file", wantCacheAnnotation: true},
46+
{inputPath: "/System/Volumes/Data/private/tmp/file", wantCacheAnnotation: true},
47+
{inputPath: "/Users/username/Documents", wantCacheAnnotation: false},
48+
49+
// Windows
50+
{inputPath: "C:\\Users\\testuser\\AppData\\Local\\Temp\\tempfile.txt", separator: '\\', wantCacheAnnotation: true},
51+
{inputPath: "C:\\Windows\\Temp\\log.txt", separator: '\\', wantCacheAnnotation: true},
52+
{inputPath: "C:\\Program Files\\MyApp", separator: '\\', wantCacheAnnotation: false},
53+
54+
// Edge cases
55+
{inputPath: "", wantCacheAnnotation: false},
56+
{inputPath: "some/relative/path", wantCacheAnnotation: false},
57+
}
58+
59+
for _, tt := range testCases {
60+
t.Run(tt.inputPath, func(t *testing.T) {
61+
if tt.separator == 0 {
62+
tt.separator = '/'
63+
}
64+
65+
if os.PathSeparator != tt.separator {
66+
t.Skipf("Skipping IsInsideCacheDir(%q)", tt.inputPath)
67+
}
68+
69+
inv := &inventory.Inventory{
70+
Packages: []*extractor.Package{&extractor.Package{
71+
Locations: []string{tt.inputPath},
72+
}},
73+
}
74+
if err := cachedir.New().Annotate(context.Background(), nil, inv); err != nil {
75+
t.Errorf("Annotate(%v): %v", inv, err)
76+
}
77+
var want []extractor.Annotation
78+
if tt.wantCacheAnnotation {
79+
want = []extractor.Annotation{extractor.InsideCacheDir}
80+
}
81+
82+
got := inv.Packages[0].Annotations
83+
if diff := cmp.Diff(want, got); diff != "" {
84+
t.Errorf("Annotate(%v) (-want +got):\n%s", inv, diff)
85+
}
86+
})
87+
}
88+
}

0 commit comments

Comments
 (0)