Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions detector/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,29 @@ import (
"github.com/future-architect/vuls/errof"
"github.com/future-architect/vuls/logging"
"github.com/future-architect/vuls/models"
"golang.org/x/oauth2"
)

type bearerTransport struct {
token string
base http.RoundTripper
}

func (t *bearerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
r := req.Clone(req.Context())
r.Header.Set("Authorization", "Bearer "+t.token)
return t.base.RoundTrip(r)
}

func newBearerClient(token string) *http.Client {
return &http.Client{
Transport: &bearerTransport{token: token, base: http.DefaultTransport},
}
}

// DetectGitHubSecurityAlerts access to owner/repo on GitHub and fetch security alerts of the repository via GitHub API v4 GraphQL and then set to the given ScanResult.
// https://help.github.com/articles/about-security-alerts-for-vulnerable-dependencies/
func DetectGitHubSecurityAlerts(r *models.ScanResult, owner, repo, token string, ignoreDismissed bool) (nCVEs int, err error) {
src := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
//TODO Proxy
httpClient := oauth2.NewClient(context.Background(), src)
httpClient := newBearerClient(token)

// TODO Use `https://github.com/shurcooL/githubv4` if the tool supports vulnerabilityAlerts Endpoint
// Memo : https://developer.github.com/v4/explorer/
Expand Down Expand Up @@ -212,11 +224,7 @@ type SecurityAlerts struct {
// DetectGitHubDependencyGraph access to owner/repo on GitHub and fetch dependency graph of the repository via GitHub API v4 GraphQL and then set to the given ScanResult.
// https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-the-dependency-graph
func DetectGitHubDependencyGraph(r *models.ScanResult, owner, repo, token string) (err error) {
src := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
//TODO Proxy
httpClient := oauth2.NewClient(context.Background(), src)
httpClient := newBearerClient(token)

return fetchDependencyGraph(r, httpClient, owner, repo, "", "", 10, 100)
}
Expand Down
106 changes: 106 additions & 0 deletions detector/github_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//go:build !scanner

package detector

import (
"net/http"
"net/http/httptest"
"testing"
)

func TestBearerTransport_SetsAuthorizationHeader(t *testing.T) {
t.Parallel()

const token = "ghp_test1234567890"

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got := r.Header.Get("Authorization")
want := "Bearer " + token
if got != want {
t.Errorf("Authorization header = %q, want %q", got, want)
}
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()

client := &http.Client{
Transport: &bearerTransport{token: token, base: http.DefaultTransport},
}

resp, err := client.Get(ts.URL)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK)
}
}

func TestBearerTransport_ClonesRequest(t *testing.T) {
t.Parallel()

const token = "ghp_clonetest"

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()

transport := &bearerTransport{token: token, base: http.DefaultTransport}

req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
req.Header.Set("X-Custom", "original")

resp, err := transport.RoundTrip(req)
if err != nil {
t.Fatalf("RoundTrip error: %v", err)
}
defer resp.Body.Close()

// The original request must not have the Authorization header injected.
if got := req.Header.Get("Authorization"); got != "" {
t.Errorf("original request Authorization header = %q, want empty (request was mutated)", got)
}

// The original X-Custom header should still be present and unchanged.
if got := req.Header.Get("X-Custom"); got != "original" {
t.Errorf("original request X-Custom header = %q, want %q", got, "original")
}
}

func TestNewBearerClient_ReturnsValidClient(t *testing.T) {
t.Parallel()

const token = "ghp_integrationtest"

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got := r.Header.Get("Authorization")
want := "Bearer " + token
if got != want {
t.Errorf("Authorization header = %q, want %q", got, want)
}
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()

// newBearerClient uses http.DefaultTransport, so it can reach the test server.
client := newBearerClient(token)
if client == nil {
t.Fatal("newBearerClient returned nil")
}

resp, err := client.Get(ts.URL)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK)
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ require (
github.com/vulsio/go-msfdb v0.4.4
github.com/vulsio/gost v0.7.2
go.etcd.io/bbolt v1.4.3
golang.org/x/oauth2 v0.35.0
golang.org/x/sync v0.20.0
golang.org/x/term v0.40.0
golang.org/x/text v0.34.0
Expand Down Expand Up @@ -356,6 +355,7 @@ require (
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.42.0 // indirect
Expand Down
Loading