-
Notifications
You must be signed in to change notification settings - Fork 176
/
osv.go
137 lines (116 loc) · 3.35 KB
/
osv.go
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
package osv
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
const (
// QueryEndpoint is the URL for posting queries to OSV.
QueryEndpoint = "https://api.osv.dev/v1/querybatch"
// GetEndpoint is the URL for getting vulenrabilities from OSV.
GetEndpoint = "https://api.osv.dev/v1/vulns"
// BaseVulnerabilityURL is the base URL for detailed vulnerability views.
BaseVulnerabilityURL = "https://osv.dev/vulnerability/"
// MaxQueriesPerRequest splits up querybatch into multiple requests if
// number of queries exceed this number
MaxQueriesPerRequest = 1000
)
// MakeCommitRequest makes a commit hash request.
func MakeCommitRequest(commit string) *Query {
return &Query{
Commit: commit,
}
}
// MakePURLRequest makes a PURL request.
func MakePURLRequest(purl string) *Query {
return &Query{
Package: Package{
PURL: purl,
},
}
}
// From: https://stackoverflow.com/a/72408490
func chunkBy[T any](items []T, chunkSize int) [][]T {
_chunks := make([][]T, 0, (len(items)/chunkSize)+1)
for chunkSize < len(items) {
items, _chunks = items[chunkSize:], append(_chunks, items[0:chunkSize:chunkSize])
}
return append(_chunks, items)
}
// checkResponseError checks if the response has an error.
func checkResponseError(resp *http.Response) error {
if resp.StatusCode == http.StatusOK {
return nil
}
respBuf, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read error response from server: %w", err)
}
return fmt.Errorf("server response error: %s", string(respBuf))
}
func MakeRequest(request BatchedQuery) (*BatchedResponse, error) {
// API has a limit of 1000 bulk query per request
queryChunks := chunkBy(request.Queries, MaxQueriesPerRequest)
var totalOsvResp BatchedResponse
for _, queries := range queryChunks {
requestBytes, err := json.Marshal(BatchedQuery{Queries: queries})
if err != nil {
return nil, err
}
requestBuf := bytes.NewBuffer(requestBytes)
resp, err := http.Post(QueryEndpoint, "application/json", requestBuf)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := checkResponseError(resp); err != nil {
return nil, err
}
var osvResp BatchedResponse
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&osvResp)
if err != nil {
return nil, err
}
totalOsvResp.Results = append(totalOsvResp.Results, osvResp.Results...)
}
return &totalOsvResp, nil
}
// Get a Vulnerabiltiy for the given ID.
func Get(id string) (*Vulnerability, error) {
resp, err := http.Get(GetEndpoint + "/" + id)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := checkResponseError(resp); err != nil {
return nil, err
}
var vuln Vulnerability
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&vuln)
if err != nil {
return nil, err
}
return &vuln, nil
}
// Hydrate fills the results of the batched response with the full
// Vulnerability details.
func Hydrate(resp *BatchedResponse) (*HydratedBatchedResponse, error) {
// TODO(ochang): Parallelize requests, or implement batch GET.
hydrated := HydratedBatchedResponse{}
for _, response := range resp.Results {
result := Response{}
for _, vuln := range response.Vulns {
vuln, err := Get(vuln.ID)
if err != nil {
return nil, err
}
result.Vulns = append(result.Vulns, *vuln)
}
hydrated.Results = append(hydrated.Results, result)
}
return &hydrated, nil
}