From 3822dd827082d3b2e6ba3100fa6dad8083b027ad Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Mon, 11 May 2026 07:52:27 -0300 Subject: [PATCH 1/7] feat: add more checks for soc2 gaps Signed-off-by: Gustavo Carvalho --- main.go | 10 +++++++++- main_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 113543d..a2687c2 100644 --- a/main.go +++ b/main.go @@ -55,6 +55,8 @@ type DependabotData struct { var errDependabotAlertsPermissionDenied = errors.New("insufficient permissions to fetch dependabot alerts") +var dependabotAlertStates = []string{"auto_dismissed", "dismissed", "fixed", "open"} + var ( granularActivities = []*proto.Activity{ {Title: "Collect Individual Dependabot Alert"}, @@ -317,7 +319,9 @@ func (l *DependabotPlugin) FetchSecurityTeamMembers(ctx context.Context) ([]*git } func (l *DependabotPlugin) FetchRepositoryDependabotAlerts(ctx context.Context, repo *github.Repository) ([]*github.DependabotAlert, error) { + stateFilter := dependabotAlertStateFilter() opts := &github.ListAlertsOptions{ + State: &stateFilter, ListCursorOptions: github.ListCursorOptions{ PerPage: 100, }, @@ -339,10 +343,14 @@ func (l *DependabotPlugin) FetchRepositoryDependabotAlerts(ctx context.Context, } } - l.logger.Debug("Fetched repository dependabot alerts from Github API", "repo", repo.GetFullName(), "count", len(allAlerts)) + l.logger.Debug("Fetched repository dependabot alerts from Github API", "repo", repo.GetFullName(), "state", stateFilter, "count", len(allAlerts)) return allAlerts, nil } +func dependabotAlertStateFilter() string { + return strings.Join(dependabotAlertStates, ",") +} + func advanceDependabotAlertsPage(opts *github.ListAlertsOptions, resp *github.Response) bool { if resp == nil { return false diff --git a/main_test.go b/main_test.go index cad1882..3b9a537 100644 --- a/main_test.go +++ b/main_test.go @@ -3,6 +3,9 @@ package main import ( "context" "errors" + "net/http" + "net/http/httptest" + "net/url" "testing" "github.com/compliance-framework/agent/runner/proto" @@ -210,6 +213,40 @@ func (s *DependabotPluginSuite) TestGranularPolicyInput_WrapsAlertInSingleElemen assert.Same(s.T(), alert, input[0]) } +func (s *DependabotPluginSuite) TestDependabotAlertStateFilter_IncludesLifecycleStates() { + assert.Equal(s.T(), "auto_dismissed,dismissed,fixed,open", dependabotAlertStateFilter()) +} + +func (s *DependabotPluginSuite) TestFetchRepositoryDependabotAlerts_UsesSingleCombinedStateFilter() { + requests := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ + assert.Equal(s.T(), "/repos/test-org/test-repo/dependabot/alerts", r.URL.Path) + assert.Equal(s.T(), "auto_dismissed,dismissed,fixed,open", r.URL.Query().Get("state")) + assert.Equal(s.T(), "100", r.URL.Query().Get("per_page")) + w.Header().Set("Content-Type", "application/json") + _, err := w.Write([]byte(`[{"number":1,"state":"fixed"},{"number":2,"state":"dismissed"}]`)) + require.NoError(s.T(), err) + })) + defer server.Close() + + baseURL, err := url.Parse(server.URL + "/") + require.NoError(s.T(), err) + + plugin := s.newPlugin(OperationalModeBundled) + plugin.githubClient = github.NewClient(server.Client()) + plugin.githubClient.BaseURL = baseURL + plugin.githubClient.UploadURL = baseURL + + alerts, err := plugin.FetchRepositoryDependabotAlerts(context.Background(), s.newRepo()) + + require.NoError(s.T(), err) + require.Len(s.T(), alerts, 2) + assert.Equal(s.T(), 1, requests) + assert.Equal(s.T(), "fixed", alerts[0].GetState()) + assert.Equal(s.T(), "dismissed", alerts[1].GetState()) +} + func (s *DependabotPluginSuite) TestAdvanceDependabotAlertsPage_UsesCursor() { opts := &github.ListAlertsOptions{} From f0aacd8de2d6c7b0504fd8525167f2fd99c54c05 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Tue, 12 May 2026 05:39:38 -0300 Subject: [PATCH 2/7] feat: skip support Signed-off-by: Gustavo Carvalho --- go.mod | 14 +++++----- go.sum | 40 +++++++++++++++------------- main.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++-- main_test.go | 48 +++++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index 0ed9a17..fa3330e 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/compliance-framework/plugin-dependabot -go 1.25.8 +go 1.26.1 require ( - github.com/compliance-framework/agent v0.3.2 + github.com/compliance-framework/agent v0.6.2 github.com/google/go-github/v71 v71.0.0 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.7.0 @@ -14,7 +14,7 @@ require ( require ( github.com/agnivade/levenshtein v1.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/compliance-framework/api v0.14.1 // indirect + github.com/compliance-framework/api v0.16.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect @@ -55,13 +55,11 @@ require ( go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/net v0.51.0 // indirect - golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/net v0.52.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect diff --git a/go.sum b/go.sum index e205b1b..3a1bc40 100644 --- a/go.sum +++ b/go.sum @@ -56,10 +56,10 @@ github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqy github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/compliance-framework/agent v0.3.2 h1:TBMQAiO5l1k3KB0/jUesEyfinoYVbOTJ4iiVdTcZ0qk= -github.com/compliance-framework/agent v0.3.2/go.mod h1:RM2C+FSFUZzsWrvCS0la+GKf3B8zC24vdPhfDXTnFU0= -github.com/compliance-framework/api v0.14.1 h1:Lig39GBwYpVVn8RdC94bcBKWMvt9KuCeG/k5wjUtFCU= -github.com/compliance-framework/api v0.14.1/go.mod h1:CMHwcOOCcVRf1u/n3BeqbrP09WWCuwnFAlD7dQfIWCA= +github.com/compliance-framework/agent v0.6.2 h1:4Ha3kTDpoAXDsGOnczeVXdf56dl7h2XNxIfawWJc+LI= +github.com/compliance-framework/agent v0.6.2/go.mod h1:k6sNhVQXviFHbz/Fe/jOkfBZ+AFLnRPIuOH2aaaCTNo= +github.com/compliance-framework/api v0.16.0 h1:0HO5a5N80ktJLeLD5GVeTk7cK7PO9Xj5WN4SR+KGBH0= +github.com/compliance-framework/api v0.16.0/go.mod h1:BupcN8mQFgB0/2+YShU/r4BUYoGwzSjbz2esdOUaX/4= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -113,8 +113,8 @@ github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -170,6 +170,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= @@ -180,8 +182,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= @@ -306,6 +308,8 @@ github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0Zqm github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/slack-go/slack v0.20.0 h1:gbDdbee8+Z2o+DWx05Spq3GzbrLLleiRwHUKs+hZLSU= +github.com/slack-go/slack v0.20.0/go.mod h1:K81UmCivcYd/5Jmz8vLBfuyoZ3B4rQC2GHVXHteXiAE= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -385,12 +389,12 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= @@ -403,12 +407,12 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= diff --git a/main.go b/main.go index a2687c2..9e7c430 100644 --- a/main.go +++ b/main.go @@ -49,8 +49,8 @@ type DependabotPlugin struct { } type DependabotData struct { - Alerts []*github.DependabotAlert - SecurityTeamMembers []*github.User + Alerts []*github.DependabotAlert `json:"alerts"` + SecurityTeamMembers []*github.User `json:"security_team_members"` } var errDependabotAlertsPermissionDenied = errors.New("insufficient permissions to fetch dependabot alerts") @@ -589,6 +589,7 @@ func (l *DependabotPlugin) EvaluatePolicies(ctx context.Context, repo *github.Re } } + appendEvidenceLink(evidences, repositorySecurityLink(repo)) l.logger.Info("collected evidence", "count", len(evidences)) return evidences, accumulatedErrors @@ -628,11 +629,81 @@ func (l *DependabotPlugin) EvaluateGranularPolicies(ctx context.Context, repo *g } } + appendEvidenceLink(evidences, dependabotAlertLink(repo, alert)) l.logger.Debug("collected granular evidence", "cve_id", cveID, "repo", repo.GetFullName(), "count", len(evidences)) return evidences, accumulatedErrors } +func appendEvidenceLink(evidences []*proto.Evidence, link *proto.Link) { + if link == nil || link.GetHref() == "" { + return + } + for _, evidence := range evidences { + if evidence == nil { + continue + } + evidence.Links = append(evidence.Links, &proto.Link{ + Href: link.GetHref(), + Rel: policyManager.Pointer(link.GetRel()), + Text: policyManager.Pointer(link.GetText()), + }) + } +} + +func repositorySecurityLink(repo *github.Repository) *proto.Link { + repositoryURL := repositoryWebURL(repo) + if repositoryURL == "" { + return nil + } + return &proto.Link{ + Href: fmt.Sprintf("%s/security", repositoryURL), + Rel: policyManager.Pointer("reference"), + Text: policyManager.Pointer("Repository security page"), + } +} + +func dependabotAlertLink(repo *github.Repository, alert *github.DependabotAlert) *proto.Link { + if alert == nil { + return nil + } + if alert.GetHTMLURL() != "" { + return &proto.Link{ + Href: alert.GetHTMLURL(), + Rel: policyManager.Pointer("reference"), + Text: policyManager.Pointer("Dependabot alert"), + } + } + if alert.GetNumber() == 0 { + return nil + } + repositoryURL := repositoryWebURL(repo) + if repositoryURL == "" { + return nil + } + return &proto.Link{ + Href: fmt.Sprintf("%s/security/dependabot/%d", repositoryURL, alert.GetNumber()), + Rel: policyManager.Pointer("reference"), + Text: policyManager.Pointer("Dependabot alert"), + } +} + +func repositoryWebURL(repo *github.Repository) string { + if repo == nil { + return "" + } + if repo.GetHTMLURL() != "" { + return strings.TrimRight(repo.GetHTMLURL(), "/") + } + if repo.GetFullName() != "" { + return fmt.Sprintf("https://github.com/%s", repo.GetFullName()) + } + if repo.GetOwner().GetLogin() != "" && repo.GetName() != "" { + return fmt.Sprintf("https://github.com/%s/%s", repo.GetOwner().GetLogin(), repo.GetName()) + } + return "" +} + func newGranularPolicyContext(repo *github.Repository) *granularPolicyContext { repositoryIdentifier := fmt.Sprintf("github-repository/%s", repo.GetFullName()) return &granularPolicyContext{ diff --git a/main_test.go b/main_test.go index 3b9a537..8317574 100644 --- a/main_test.go +++ b/main_test.go @@ -62,6 +62,7 @@ func (s *DependabotPluginSuite) newRepo() *github.Repository { return &github.Repository{ Name: ptr("test-repo"), FullName: ptr("test-org/test-repo"), + HTMLURL: ptr("https://github.com/test-org/test-repo"), Owner: &github.User{ Login: ptr("test-org"), Name: ptr("test-org"), @@ -213,6 +214,53 @@ func (s *DependabotPluginSuite) TestGranularPolicyInput_WrapsAlertInSingleElemen assert.Same(s.T(), alert, input[0]) } +func (s *DependabotPluginSuite) TestRepositorySecurityLink_UsesRepositorySecurityPage() { + link := repositorySecurityLink(s.newRepo()) + + require.NotNil(s.T(), link) + assert.Equal(s.T(), "https://github.com/test-org/test-repo/security", link.GetHref()) + assert.Equal(s.T(), "reference", link.GetRel()) + assert.Equal(s.T(), "Repository security page", link.GetText()) +} + +func (s *DependabotPluginSuite) TestDependabotAlertLink_UsesHTMLURL() { + alert := s.newDetailedAlert("CVE-2026-0002", "GHSA-456", "high", "requests", "pip", nil) + alert.HTMLURL = ptr("https://github.com/test-org/test-repo/security/dependabot/7") + + link := dependabotAlertLink(s.newRepo(), alert) + + require.NotNil(s.T(), link) + assert.Equal(s.T(), "https://github.com/test-org/test-repo/security/dependabot/7", link.GetHref()) + assert.Equal(s.T(), "reference", link.GetRel()) + assert.Equal(s.T(), "Dependabot alert", link.GetText()) +} + +func (s *DependabotPluginSuite) TestDependabotAlertLink_BuildsNumberFallback() { + alert := s.newDetailedAlert("CVE-2026-0002", "GHSA-456", "high", "requests", "pip", nil) + alert.Number = github.Ptr(42) + + link := dependabotAlertLink(s.newRepo(), alert) + + require.NotNil(s.T(), link) + assert.Equal(s.T(), "https://github.com/test-org/test-repo/security/dependabot/42", link.GetHref()) +} + +func (s *DependabotPluginSuite) TestAppendEvidenceLink_AddsLinkToEvidence() { + evidences := []*proto.Evidence{ + {}, + nil, + {}, + } + + appendEvidenceLink(evidences, repositorySecurityLink(s.newRepo())) + + require.Len(s.T(), evidences[0].GetLinks(), 1) + assert.Equal(s.T(), "https://github.com/test-org/test-repo/security", evidences[0].GetLinks()[0].GetHref()) + assert.Empty(s.T(), evidences[1].GetLinks()) + require.Len(s.T(), evidences[2].GetLinks(), 1) + assert.Equal(s.T(), "https://github.com/test-org/test-repo/security", evidences[2].GetLinks()[0].GetHref()) +} + func (s *DependabotPluginSuite) TestDependabotAlertStateFilter_IncludesLifecycleStates() { assert.Equal(s.T(), "auto_dismissed,dismissed,fixed,open", dependabotAlertStateFilter()) } From 4d7598d1fe839400646a302d5adb10f735b86602 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Tue, 12 May 2026 05:46:13 -0300 Subject: [PATCH 3/7] fix: go version on CI Signed-off-by: Gustavo Carvalho --- .github/workflows/build-and-upload.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-and-upload.yml b/.github/workflows/build-and-upload.yml index c9e037e..4affeae 100644 --- a/.github/workflows/build-and-upload.yml +++ b/.github/workflows/build-and-upload.yml @@ -9,6 +9,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 + with: + go-version-file: go.mod - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: From 0869ec30684ea7cf3b2d86059d4742e38ce2de07 Mon Sep 17 00:00:00 2001 From: Gustavo Fernandes de Carvalho <17139678+gusfcarvalho@users.noreply.github.com> Date: Tue, 12 May 2026 06:08:59 -0300 Subject: [PATCH 4/7] fix: security team member now omits empty Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 9e7c430..6f2a205 100644 --- a/main.go +++ b/main.go @@ -50,7 +50,7 @@ type DependabotPlugin struct { type DependabotData struct { Alerts []*github.DependabotAlert `json:"alerts"` - SecurityTeamMembers []*github.User `json:"security_team_members"` + SecurityTeamMembers []*github.User `json:"security_team_members,omitempty"` } var errDependabotAlertsPermissionDenied = errors.New("insufficient permissions to fetch dependabot alerts") From 4c6b3bd3271a682f6c5f06f835359cbde5fa0979 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Tue, 12 May 2026 06:18:11 -0300 Subject: [PATCH 5/7] fix: issues Signed-off-by: Gustavo Carvalho --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84d5222..68d6912 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 + with: + go-version-file: go.mod - name: Test run: go test ./... From dc766b4a0140d2541e5ab8b1c2e7c81cea55c6d4 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Tue, 12 May 2026 06:27:35 -0300 Subject: [PATCH 6/7] fix: copilot issues Signed-off-by: Gustavo Carvalho --- main.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 6f2a205..56d71b6 100644 --- a/main.go +++ b/main.go @@ -49,8 +49,11 @@ type DependabotPlugin struct { } type DependabotData struct { - Alerts []*github.DependabotAlert `json:"alerts"` - SecurityTeamMembers []*github.User `json:"security_team_members,omitempty"` + Alerts []*github.DependabotAlert `json:"alerts"` + // omitempty is intentional: field absent means security team not configured, + // field present (even empty) means security team was requested but has no members. + // This allows policies to distinguish between these two cases. + SecurityTeamMembers []*github.User `json:"security_team_members,omitempty"` } var errDependabotAlertsPermissionDenied = errors.New("insufficient permissions to fetch dependabot alerts") @@ -343,7 +346,7 @@ func (l *DependabotPlugin) FetchRepositoryDependabotAlerts(ctx context.Context, } } - l.logger.Debug("Fetched repository dependabot alerts from Github API", "repo", repo.GetFullName(), "state", stateFilter, "count", len(allAlerts)) + l.logger.Debug("Fetched repository dependabot alerts from GitHub API", "repo", repo.GetFullName(), "state", stateFilter, "count", len(allAlerts)) return allAlerts, nil } From e49c6b41a8a7272f6c5a4ef772a70b58eb20ee80 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Tue, 12 May 2026 06:34:20 -0300 Subject: [PATCH 7/7] fix: copilot issues Signed-off-by: Gustavo Carvalho --- main.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 56d71b6..beaa5a3 100644 --- a/main.go +++ b/main.go @@ -50,10 +50,9 @@ type DependabotPlugin struct { type DependabotData struct { Alerts []*github.DependabotAlert `json:"alerts"` - // omitempty is intentional: field absent means security team not configured, - // field present (even empty) means security team was requested but has no members. - // This allows policies to distinguish between these two cases. - SecurityTeamMembers []*github.User `json:"security_team_members,omitempty"` + // Pointer-to-slice allows distinguishing: nil (not configured/fetched) vs &[] (empty team) vs &[members] (has members). + // omitempty omits nil, but emits empty or non-empty slices. + SecurityTeamMembers *[]*github.User `json:"security_team_members,omitempty"` } var errDependabotAlertsPermissionDenied = errors.New("insufficient permissions to fetch dependabot alerts") @@ -292,7 +291,7 @@ func (l *DependabotPlugin) evalForBundle(ctx context.Context, repo *github.Repos Alerts: alerts, } if securityTeamMembers != nil { - data.SecurityTeamMembers = securityTeamMembers + data.SecurityTeamMembers = &securityTeamMembers } evidences, err := l.EvaluatePolicies(ctx, repo, data, req)