Add ability to tweet published reports
When a report is automatically published, if a Twitter access token is
set, a Tweet will be published with the title of the report and a link
to the full report.
Change-Id: Ife68d49c4d04a40b1a41a964225fd47dd514d819
diff --git a/.env b/.env
deleted file mode 100644
index f51bd45..0000000
--- a/.env
+++ /dev/null
@@ -1 +0,0 @@
-GOOGLE_APPLICATION_CREDENTIALS=/secret/credentials.json
diff --git a/.env.sample b/.env.sample
new file mode 100644
index 0000000..0c297ca
--- /dev/null
+++ b/.env.sample
@@ -0,0 +1,8 @@
+GOOGLE_APPLICATION_CREDENTIALS=/secret/credentials.json
+
+# You can leave the following fields empty if you don't want to publish tweets
+# when a vulnerability is published:
+TWITTER_OAUTH_TOKEN=
+TWITTER_OAUTH_TOKEN_SECRET=
+GOTWI_API_KEY=
+GOTWI_API_KEY_SECRET=
diff --git a/.gitignore b/.gitignore
index 0e6c978..7b84e7f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
secret/
+.env
diff --git a/README.md b/README.md
index 9f48535..03d6a48 100644
--- a/README.md
+++ b/README.md
@@ -30,4 +30,5 @@
- Give it appropiate permissions in each Monorail project.
- Create subdirectory `//secret/` and download the service accounts
credentials JSON file to `//secret/credentials.json`.
+ - Copy the `.env.sample` file to `.env` and edit it to your liking.
- Run `make docker-prod` and `docker-compose up -d`.
diff --git a/go.mod b/go.mod
index 8a4c24f..bb8e3b9 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@
require (
github.com/cenkalti/backoff/v4 v4.1.3
+ github.com/michimani/gotwi v0.13.0
go.skia.org/infra v0.0.0-20220714212951-8117921d36db
golang.org/x/exp v0.0.0-20220713135740-79cabaa25d75
google.golang.org/api v0.87.0
diff --git a/go.sum b/go.sum
index 65c4d88..43972fe 100644
--- a/go.sum
+++ b/go.sum
@@ -78,6 +78,7 @@
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -187,6 +188,9 @@
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/michimani/gotwi v0.13.0 h1:zVVkibHLpP6Q3pg1/3rzuVcb+95p4TZchViW/ZE53/4=
+github.com/michimani/gotwi v0.13.0/go.mod h1:2W7Xp7vgg7ZZFdwXYHwbr9gUCMaZc+ql+T8RpccbeQ4=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
@@ -198,6 +202,7 @@
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -650,6 +655,7 @@
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/stringUtils.go b/stringUtils.go
new file mode 100644
index 0000000..791600e
--- /dev/null
+++ b/stringUtils.go
@@ -0,0 +1,23 @@
+package main
+
+func truncateStringWithEllipsis(s string, maxChars int) string {
+ if len(s) <= maxChars {
+ return s
+ }
+
+ // Find the last space within the maximum character limit
+ i := maxChars - 1
+ for ; i >= 0; i-- {
+ if s[i] == ' ' {
+ break
+ }
+ }
+
+ // If no space found, truncate to the maximum length minus the ellipsis length
+ if i < 0 {
+ return s[:maxChars-1] + "…"
+ }
+
+ // Truncate at the last space found
+ return s[:i] + "…"
+}
diff --git a/vulnzybot.go b/vulnzybot.go
index a954b35..1c10799 100644
--- a/vulnzybot.go
+++ b/vulnzybot.go
@@ -9,6 +9,9 @@
"time"
"github.com/cenkalti/backoff/v4"
+ "github.com/michimani/gotwi"
+ "github.com/michimani/gotwi/tweet/managetweet"
+ "github.com/michimani/gotwi/tweet/managetweet/types"
"go.skia.org/infra/go/monorail/v1"
"golang.org/x/exp/slices"
"google.golang.org/api/option"
@@ -34,6 +37,8 @@
type VulnerabilityReport struct {
// ID of the vulnerability report/issue in Monorail
IssueId int64
+ // Title of the report
+ Title string
// Issue status
Status string
// Date that the status was modified
@@ -63,6 +68,7 @@
func parseIssue(issue *monorail.ProtoApiPb2V1IssueWrapper) (*VulnerabilityReport, error) {
report := VulnerabilityReport{IssueId: issue.Id}
+ report.Title = issue.Title
report.Status = issue.Status
var err error
@@ -220,7 +226,21 @@
return err
}
-func performAction(report *VulnerabilityReport, action *Action, monorailSvc *monorail.Service, ctx context.Context) error {
+func postTweet(report *VulnerabilityReport, twtrClient *gotwi.Client, ctx context.Context) error {
+ truncatedBugTitle := truncateStringWithEllipsis(report.Title, 240-(2+1+6+1+1+20))
+ url := fmt.Sprintf("iavm.xyz/b/vulnz/%d", report.IssueId)
+ tweetText := fmt.Sprintf("🦋 b/%d %s %s", report.IssueId, truncatedBugTitle, url)
+ body := &types.CreateInput{
+ Text: gotwi.String(tweetText),
+ }
+ _, err := managetweet.Create(ctx, twtrClient, body)
+ if err == nil {
+ logIssue(fmt.Sprintf("Tweeted: %s", tweetText), report)
+ }
+ return err
+}
+
+func performAction(report *VulnerabilityReport, action *Action, monorailSvc *monorail.Service, twtrClient *gotwi.Client, ctx context.Context) error {
var message string
if action.Reason == "fixedDeadline" {
deadlineAfterFixedInt := deadlineAfterFixed / (24 * time.Hour)
@@ -240,10 +260,18 @@
if err != nil {
return fmt.Errorf("Error trying to perform action type %s, for reason %s: %v", action.Type, action.Reason, err)
}
+
+ if twtrClient != nil && action.Reason == "disclosureDeadline" && action.Type == "disclosure" {
+ err := postTweet(report, twtrClient, ctx)
+ if err != nil {
+ return fmt.Errorf("Error trying to publish tweet: %v", err)
+ }
+ }
+
return nil
}
-func handleIssue(issue *monorail.ProtoApiPb2V1IssueWrapper, monorailSvc *monorail.Service, ctx context.Context) error {
+func handleIssue(issue *monorail.ProtoApiPb2V1IssueWrapper, monorailSvc *monorail.Service, twtrClient *gotwi.Client, ctx context.Context) error {
report, err := parseIssue(issue)
if err != nil {
return fmt.Errorf("Error parsing issue: %v", err)
@@ -252,7 +280,7 @@
actions := getTodayActions(report)
for _, action := range actions {
- err = performAction(report, action, monorailSvc, ctx)
+ err = performAction(report, action, monorailSvc, twtrClient, ctx)
if err != nil {
return fmt.Errorf("Error handling %s action: %v", action.Type, err)
}
@@ -277,6 +305,21 @@
log.Fatalf("Can't create Monorail service: %v", err)
}
+ twtrToken, okToken := os.LookupEnv("TWITTER_OAUTH_TOKEN")
+ twtrSecret, okSecret := os.LookupEnv("TWITTER_OAUTH_TOKEN_SECRET")
+ var twtrClient *gotwi.Client
+ if okToken && okSecret {
+ in := &gotwi.NewClientInput{
+ AuthenticationMethod: gotwi.AuthenMethodOAuth1UserContext,
+ OAuthToken: twtrToken,
+ OAuthTokenSecret: twtrSecret,
+ }
+ twtrClient, err = gotwi.NewClient(in)
+ if err != nil {
+ log.Fatalf("Can't create Twitter client: %v", err)
+ }
+ }
+
var listResponse *monorail.ProtoApiPb2V1IssuesListResponse
exp := newRequestExponentialBackOff()
@@ -290,7 +333,7 @@
}
for _, issue := range listResponse.Items {
- err := handleIssue(issue, monorailSvc, ctx)
+ err := handleIssue(issue, monorailSvc, twtrClient, ctx)
if err != nil {
log.Printf("[Issue %v] Error while handling issue: %v", issue.Id, err)
}