blob: a954b35efeb05f7ba5d2b76de8988198cf49af64 [file] [log] [blame]
Adrià Vilanova Martíneze73997b2022-07-19 18:58:55 +02001package main
2
3import (
4 "context"
5 "fmt"
6 "log"
7 "os"
8 "strconv"
9 "time"
10
11 "github.com/cenkalti/backoff/v4"
12 "go.skia.org/infra/go/monorail/v1"
13 "golang.org/x/exp/slices"
14 "google.golang.org/api/option"
15)
16
17const (
18 monorailEndpointURL = "https://bugs.avm99963.com/_ah/api/monorail/v1/"
19 vulnzProject = "vulnz"
20 doNotPublishLabel = "DoNotPublish"
21 restrictedLabel = "Restrict-View-Commit"
22 // Days after which the report for a fixed vulnerability will be automatically
23 // published.
24 deadlineAfterFixed = 30 * 24 * time.Hour
25 // Grace period added to the deadlines
26 gracePeriod = 1 * 24 * time.Hour
27 // Time to warn about the automatic disclosure before it happens
28 warningPeriod = 5 * 24 * time.Hour
29 // Layouts returned by the monorail package
30 dateTimeLayout = "2006-01-02T15:04:05"
31 dateLayout = "2006-01-02"
32)
33
34type VulnerabilityReport struct {
35 // ID of the vulnerability report/issue in Monorail
36 IssueId int64
37 // Issue status
38 Status string
39 // Date that the status was modified
40 StatusModified time.Time
41 // Date when the vulnerability report was sent to the vendor
42 Reported time.Time
43 // Number of days since the reported date when the vulnerability should be automatically disclosed
44 DeadlineDays int
45 // DeadlineDays, converted to time.Duration
46 DeadlineDuration time.Duration
47 // Date when the vulnerability report should be published if it has been fixed
48 PublishDateIfFixed time.Time
49 // Deadline when the vulnerability report should be published if not fixed
50 DeadlineTime time.Time
51 HasDoNotPublishLabel bool
52 HasRestrictedLabel bool
53}
54
55type Action struct {
56 MustPerform bool
57 // |Reason| can be `disclosureDeadline` (the deadline has been exceeded), `fixedDeadline` (the issue has been fixed and |deadlineAfterFixed| has passed)
58 Reason string
59 // |Type| can be `warning` (warning before actually disclosing the report), `disclosure` (the report is actually disclosed)
60 Type string
61}
62
63func parseIssue(issue *monorail.ProtoApiPb2V1IssueWrapper) (*VulnerabilityReport, error) {
64 report := VulnerabilityReport{IssueId: issue.Id}
65
66 report.Status = issue.Status
67
68 var err error
69 report.StatusModified, err = time.Parse(dateTimeLayout, issue.StatusModified)
70 if err != nil {
71 return nil, err
72 }
73
74 deadlineFound := false
75 reportedFound := false
76 for _, field := range issue.FieldValues {
77 if field.FieldName == "Deadline" {
78 deadlineFound = true
79 report.DeadlineDays, err = strconv.Atoi(field.FieldValue)
80 report.DeadlineDuration = time.Duration(report.DeadlineDays) * 24 * time.Hour
81 if err != nil {
82 return nil, fmt.Errorf("Error parsing Deadline field: %v")
83 }
84 }
85
86 if field.FieldName == "Reported" {
87 reportedFound = true
88 report.Reported, err = time.Parse(dateLayout, field.FieldValue)
89 if err != nil {
90 return nil, fmt.Errorf("Error parsing Reported field: %v")
91 }
92 }
93 }
94 if !deadlineFound {
95 return nil, fmt.Errorf("The Deadline field isn't set.")
96 }
97 if !reportedFound {
98 return nil, fmt.Errorf("The Reported field isn't set.")
99 }
100
101 report.PublishDateIfFixed = report.StatusModified.Add(deadlineAfterFixed + gracePeriod)
102 report.DeadlineTime = report.Reported.Add(report.DeadlineDuration + gracePeriod)
103
104 report.HasDoNotPublishLabel = slices.Contains(issue.Labels, doNotPublishLabel)
105 report.HasRestrictedLabel = slices.Contains(issue.Labels, restrictedLabel)
106
107 return &report, nil
108}
109
110func logIssue(message string, report *VulnerabilityReport) {
111 log.Printf("[Issue %d] %s", report.IssueId, message)
112}
113
114// |actionType| can be one of the types allowed in |Action.Type|.
115func getActionPartial(report *VulnerabilityReport, actionType string) *Action {
116 now := time.Now()
117 isWarning := actionType == "warning"
118
119 // Take into account that warnings are published in advance (according to warningPeriod)
120 var (
121 publishDateIfFixed time.Time
122 deadlineTime time.Time
123 )
124 if isWarning {
125 publishDateIfFixed = report.PublishDateIfFixed.Add(-warningPeriod)
126 deadlineTime = report.DeadlineTime.Add(-warningPeriod)
127 } else {
128 publishDateIfFixed = report.PublishDateIfFixed
129 deadlineTime = report.DeadlineTime
130 }
131
132 if report.Status == "Fixed" || report.Status == "Verified" {
133 if (!isWarning && publishDateIfFixed.Before(now)) || (isWarning && dateEqual(publishDateIfFixed, now)) {
134 return &Action{
135 MustPerform: true,
136 Reason: "fixedDeadline",
137 Type: actionType,
138 }
139 }
140 return &Action{MustPerform: false}
141 }
142 if (!isWarning && deadlineTime.Before(now)) || (isWarning && dateEqual(deadlineTime, now)) {
143 return &Action{
144 MustPerform: true,
145 Reason: "disclosureDeadline",
146 Type: actionType,
147 }
148 }
149 return &Action{MustPerform: false}
150}
151
152// Get the actions that should be performed today on |report|
153func getTodayActions(report *VulnerabilityReport) []*Action {
154 // By default, all the actions will be set to |false|
155 var actions []*Action
156
157 // Do not publish any issue which has the DoNotPublish label set!
158 if report.HasDoNotPublishLabel {
159 logIssue(fmt.Sprintf("The %s label is set. Skipping.\n", doNotPublishLabel), report)
160 return actions
161 }
162
163 // Check that the report is not already published
164 if !report.HasRestrictedLabel {
165 logIssue(fmt.Sprintf("The %s label isn't set (so in theory it shouldn't have been returned by the query). Skipping.\n", restrictedLabel), report)
166 return actions
167 }
168
169 warningAction := getActionPartial(report, "warning")
170 if warningAction.MustPerform {
171 actions = append(actions, warningAction)
172 }
173 disclosureAction := getActionPartial(report, "disclosure")
174 if disclosureAction.MustPerform {
175 actions = append(actions, disclosureAction)
176 }
177 return actions
178}
179
180func newRequestExponentialBackOff() *backoff.ExponentialBackOff {
181 b := backoff.NewExponentialBackOff()
182 b.InitialInterval = 500 * time.Millisecond
183 b.RandomizationFactor = 0.5
184 b.Multiplier = 1.5
185 b.MaxInterval = 30 * time.Second
186 b.MaxElapsedTime = 5 * time.Minute
187 b.Reset()
188 return b
189}
190
191func publishReportAttempt(message string, report *VulnerabilityReport, action *Action, monorailSvc *monorail.Service, ctx context.Context) error {
192 ctxRequest, cancel := context.WithDeadline(ctx, time.Now().Add(45*time.Second))
193 defer cancel()
194
195 comment := monorail.ProtoApiPb2V1IssueCommentWrapper{
196 Content: message,
197 }
198
199 if action.Type == "disclosure" {
200 // Remove the label which restricts the bug from public view
201 labels := make([]string, 1)
202 labels[0] = "-" + restrictedLabel
203 comment.Updates = &monorail.ProtoApiPb2V1Update{Labels: labels}
204 }
205
206 _, err := monorailSvc.Issues.Comments.Insert(vulnzProject, report.IssueId, &comment).SendEmail(true).Context(ctxRequest).Do()
207 if err != nil {
208 return fmt.Errorf("The request to insert the comment failed: %v", err)
209 }
210 logIssue(fmt.Sprintf("Added comment (type: %s, reason: %s)", action.Type, action.Reason), report)
211
212 return nil
213}
214
215func publishReport(message string, report *VulnerabilityReport, action *Action, monorailSvc *monorail.Service, ctx context.Context) error {
216 exp := newRequestExponentialBackOff()
217 err := backoff.Retry(func() error {
218 return publishReportAttempt(message, report, action, monorailSvc, ctx)
219 }, exp)
220 return err
221}
222
223func performAction(report *VulnerabilityReport, action *Action, monorailSvc *monorail.Service, ctx context.Context) error {
224 var message string
225 if action.Reason == "fixedDeadline" {
226 deadlineAfterFixedInt := deadlineAfterFixed / (24 * time.Hour)
227 if action.Type == "warning" {
228 message = fmt.Sprintf("This report will be published in %d days, since at that time %d days will have passed since the vulnerability was fixed.\n\n_Please add the `%s` label if you want to stop the automatic disclosure._", warningPeriod/(24*time.Hour), deadlineAfterFixedInt, doNotPublishLabel)
229 } else {
230 message = fmt.Sprintf("**The vulnerability was fixed %d days ago** -- automatically publishing the vulnerability report.", deadlineAfterFixedInt)
231 }
232 } else if action.Reason == "disclosureDeadline" {
233 if action.Type == "warning" {
234 message = fmt.Sprintf("This report will be published in %d days, since at that time %d days will have passed since the vulnerability was reported.\n\n_Please add the `%s` label if you want to stop the automatic disclosure._", warningPeriod/(24*time.Hour), report.DeadlineDays, doNotPublishLabel)
235 } else {
236 message = fmt.Sprintf("**The %d-day deadline has been exceeded** -- automatically publishing the vulnerability report.", report.DeadlineDays)
237 }
238 }
239 err := publishReport(message, report, action, monorailSvc, ctx)
240 if err != nil {
241 return fmt.Errorf("Error trying to perform action type %s, for reason %s: %v", action.Type, action.Reason, err)
242 }
243 return nil
244}
245
246func handleIssue(issue *monorail.ProtoApiPb2V1IssueWrapper, monorailSvc *monorail.Service, ctx context.Context) error {
247 report, err := parseIssue(issue)
248 if err != nil {
249 return fmt.Errorf("Error parsing issue: %v", err)
250 }
251
252 actions := getTodayActions(report)
253
254 for _, action := range actions {
255 err = performAction(report, action, monorailSvc, ctx)
256 if err != nil {
257 return fmt.Errorf("Error handling %s action: %v", action.Type, err)
258 }
259 }
260
261 return nil
262}
263
264func main() {
265 log.Println("Starting vulnzybot...")
266
267 ctx := context.Background()
268
269 credentials, ok := os.LookupEnv("GOOGLE_APPLICATION_CREDENTIALS")
270 if !ok {
271 log.Println("GOOGLE_APPLICATION_CREDENTIALS environment variable is not set.")
272 return
273 }
274
275 monorailSvc, err := monorail.NewService(ctx, option.WithEndpoint(monorailEndpointURL), option.WithCredentialsFile(credentials))
276 if err != nil {
277 log.Fatalf("Can't create Monorail service: %v", err)
278 }
279
280 var listResponse *monorail.ProtoApiPb2V1IssuesListResponse
281
282 exp := newRequestExponentialBackOff()
283 err = backoff.Retry(func() error {
284 var err error
285 listResponse, err = monorailSvc.Issues.List(vulnzProject).Can("all").Q("Type=VulnerabilityReport Restrict=View-Commit").MaxResults(100000).Do()
286 return err
287 }, exp)
288 if err != nil {
289 log.Fatalf("Can't list issues: %v", err)
290 }
291
292 for _, issue := range listResponse.Items {
293 err := handleIssue(issue, monorailSvc, ctx)
294 if err != nil {
295 log.Printf("[Issue %v] Error while handling issue: %v", issue.Id, err)
296 }
297 }
298}