File: db.go

package info (click to toggle)
golang-golang-x-tools 1%3A0.25.0%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 22,724 kB
  • sloc: javascript: 2,027; asm: 1,645; sh: 166; yacc: 155; makefile: 49; ansic: 8
file content (233 lines) | stat: -rw-r--r-- 6,169 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package vulntest provides helpers for vulncheck functionality testing.
package vulntest

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"golang.org/x/tools/gopls/internal/protocol"
	"golang.org/x/tools/gopls/internal/vulncheck/osv"
	"golang.org/x/tools/txtar"
)

// NewDatabase returns a read-only DB containing the provided
// txtar-format collection of vulnerability reports.
// Each vulnerability report is a YAML file whose format
// is defined in golang.org/x/vulndb/doc/format.md.
// A report file name must have the id as its base name,
// and have .yaml as its extension.
//
//	db, err := NewDatabase(ctx, reports)
//	...
//	defer db.Clean()
//	client, err := NewClient(db)
//	...
//
// The returned DB's Clean method must be called to clean up the
// generated database.
func NewDatabase(ctx context.Context, txtarReports []byte) (*DB, error) {
	disk, err := os.MkdirTemp("", "vulndb-test")
	if err != nil {
		return nil, err
	}
	if err := generateDB(ctx, txtarReports, disk, false); err != nil {
		os.RemoveAll(disk)
		return nil, err
	}

	return &DB{disk: disk}, nil
}

// DB is a read-only vulnerability database on disk.
// Users can use this database with golang.org/x/vuln APIs
// by setting the `VULNDB` environment variable.
type DB struct {
	disk string
}

// URI returns the file URI that can be used for VULNDB environment
// variable.
func (db *DB) URI() string {
	u := protocol.URIFromPath(filepath.Join(db.disk, "ID"))
	return string(u)
}

// Clean deletes the database.
func (db *DB) Clean() error {
	return os.RemoveAll(db.disk)
}

//
// The following was selectively copied from golang.org/x/vulndb/internal/database
//

const (
	dbURL = "https://pkg.go.dev/vuln/"

	// idDirectory is the name of the directory that contains entries
	// listed by their IDs.
	idDirectory = "ID"

	// cmdModule is the name of the module containing Go toolchain
	// binaries.
	cmdModule = "cmd"

	// stdModule is the name of the module containing Go std packages.
	stdModule = "std"
)

// generateDB generates the file-based vuln DB in the directory jsonDir.
func generateDB(ctx context.Context, txtarData []byte, jsonDir string, indent bool) error {
	archive := txtar.Parse(txtarData)

	entries, err := generateEntries(ctx, archive)
	if err != nil {
		return err
	}
	return writeEntriesByID(filepath.Join(jsonDir, idDirectory), entries, indent)
}

func generateEntries(_ context.Context, archive *txtar.Archive) ([]osv.Entry, error) {
	now := time.Now()
	var entries []osv.Entry
	for _, f := range archive.Files {
		if !strings.HasSuffix(f.Name, ".yaml") {
			continue
		}
		r, err := readReport(bytes.NewReader(f.Data))
		if err != nil {
			return nil, err
		}
		name := strings.TrimSuffix(filepath.Base(f.Name), filepath.Ext(f.Name))
		linkName := fmt.Sprintf("%s%s", dbURL, name)
		entry := generateOSVEntry(name, linkName, now, *r)
		entries = append(entries, entry)
	}
	return entries, nil
}

func writeEntriesByID(idDir string, entries []osv.Entry, indent bool) error {
	// Write a directory containing entries by ID.
	if err := os.MkdirAll(idDir, 0755); err != nil {
		return fmt.Errorf("failed to create directory %q: %v", idDir, err)
	}
	for _, e := range entries {
		outPath := filepath.Join(idDir, e.ID+".json")
		if err := writeJSON(outPath, e, indent); err != nil {
			return err
		}
	}
	return nil
}

func writeJSON(filename string, value any, indent bool) (err error) {
	j, err := jsonMarshal(value, indent)
	if err != nil {
		return err
	}
	return os.WriteFile(filename, j, 0644)
}

func jsonMarshal(v any, indent bool) ([]byte, error) {
	if indent {
		return json.MarshalIndent(v, "", "  ")
	}
	return json.Marshal(v)
}

// generateOSVEntry create an osv.Entry for a report. In addition to the report, it
// takes the ID for the vuln and a URL that will point to the entry in the vuln DB.
// It returns the osv.Entry and a list of module paths that the vuln affects.
func generateOSVEntry(id, url string, lastModified time.Time, r Report) osv.Entry {
	entry := osv.Entry{
		ID:               id,
		Published:        r.Published,
		Modified:         lastModified,
		Withdrawn:        r.Withdrawn,
		Summary:          r.Summary,
		Details:          r.Description,
		DatabaseSpecific: &osv.DatabaseSpecific{URL: url},
	}

	moduleMap := make(map[string]bool)
	for _, m := range r.Modules {
		switch m.Module {
		case stdModule:
			moduleMap[osv.GoStdModulePath] = true
		case cmdModule:
			moduleMap[osv.GoCmdModulePath] = true
		default:
			moduleMap[m.Module] = true
		}
		entry.Affected = append(entry.Affected, toAffected(m))
	}
	for _, ref := range r.References {
		entry.References = append(entry.References, osv.Reference{
			Type: ref.Type,
			URL:  ref.URL,
		})
	}
	return entry
}

func AffectedRanges(versions []VersionRange) []osv.Range {
	a := osv.Range{Type: osv.RangeTypeSemver}
	if len(versions) == 0 || versions[0].Introduced == "" {
		a.Events = append(a.Events, osv.RangeEvent{Introduced: "0"})
	}
	for _, v := range versions {
		if v.Introduced != "" {
			a.Events = append(a.Events, osv.RangeEvent{Introduced: v.Introduced.Canonical()})
		}
		if v.Fixed != "" {
			a.Events = append(a.Events, osv.RangeEvent{Fixed: v.Fixed.Canonical()})
		}
	}
	return []osv.Range{a}
}

func toOSVPackages(pkgs []*Package) (imps []osv.Package) {
	for _, p := range pkgs {
		syms := append([]string{}, p.Symbols...)
		syms = append(syms, p.DerivedSymbols...)
		sort.Strings(syms)
		imps = append(imps, osv.Package{
			Path:    p.Package,
			GOOS:    p.GOOS,
			GOARCH:  p.GOARCH,
			Symbols: syms,
		})
	}
	return imps
}

func toAffected(m *Module) osv.Affected {
	name := m.Module
	switch name {
	case stdModule:
		name = osv.GoStdModulePath
	case cmdModule:
		name = osv.GoCmdModulePath
	}
	return osv.Affected{
		Module: osv.Module{
			Path:      name,
			Ecosystem: osv.GoEcosystem,
		},
		Ranges: AffectedRanges(m.Versions),
		EcosystemSpecific: osv.EcosystemSpecific{
			Packages: toOSVPackages(m.Packages),
		},
	}
}