Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Use RepoClient.Search API in SAST check #857

Merged
merged 2 commits into from
Aug 16, 2021
Merged
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
14 changes: 9 additions & 5 deletions checks/sast.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/google/go-github/v38/github"

"github.com/ossf/scorecard/v2/checker"
"github.com/ossf/scorecard/v2/clients"
sce "github.com/ossf/scorecard/v2/errors"
)

Expand Down Expand Up @@ -158,20 +159,23 @@ func sastToolInCheckRuns(c *checker.CheckRequest) (int, error) {

// nolint
func codeQLInCheckDefinitions(c *checker.CheckRequest) (int, error) {
searchQuery := ("github/codeql-action path:/.github/workflows repo:" + c.Owner + "/" + c.Repo)
results, _, err := c.Client.Search.Code(c.Ctx, searchQuery, &github.SearchOptions{})
searchRequest := clients.SearchRequest{
Query: "github/codeql-action",
Path: "/.github/workflows",
}
resp, err := c.RepoClient.Search(searchRequest)
if err != nil {
return checker.InconclusiveResultScore,
sce.Create(sce.ErrScorecardInternal, fmt.Sprintf("Client.Search.Code: %v", err))
}

for _, result := range results.CodeResults {
c.Dlogger.Debug("CodeQL detected: %s", result.GetPath())
for _, result := range resp.Results {
c.Dlogger.Debug("CodeQL detected: %s", result.Path)
}

// TODO: check if it's enabled as cron or presubmit.
// TODO: check which branches it is enabled on. We should find main.
if *results.Total > 0 {
if resp.Hits > 0 {
c.Dlogger.Info("SAST tool detected: CodeQL")
return checker.MaxResultScore, nil
}
Expand Down
21 changes: 6 additions & 15 deletions clients/githubrepo/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Client struct {
repoClient *github.Client
graphClient *graphqlHandler
contributors *contributorsHandler
search *searchHandler
ctx context.Context
tarball tarballHandler
}
Expand Down Expand Up @@ -64,6 +65,9 @@ func (client *Client) InitRepo(owner, repoName string) error {
return fmt.Errorf("error during contributorsHandler.init: %w", err)
}

// Setup Search.
client.search.init(client.ctx, owner, repoName)

return nil
}

Expand Down Expand Up @@ -113,21 +117,8 @@ func (client *Client) GetDefaultBranch() (clients.BranchRef, error) {
}

// Search implements RepoClient.Search.
func (client *Client) Search(request clients.SearchRequest) (clients.SearchResult, error) {
var query string
if request.Filename == "" {
query = fmt.Sprintf("%s repo:%s/%s", request.Query, client.owner, client.repoName)
} else {
query = fmt.Sprintf("%s repo:%s/%s in:file filename:%s",
request.Query, client.owner, client.repoName, request.Filename)
}
res, _, err := client.repoClient.Search.Code(client.ctx, query, &github.SearchOptions{})
if err != nil {
return clients.SearchResult{}, fmt.Errorf("Search.Code: %w", err)
}
return clients.SearchResult{
Hits: res.GetTotal(),
}, nil
func (client *Client) Search(request clients.SearchRequest) (clients.SearchResponse, error) {
return client.search.search(request)
}

// Close implements RepoClient.Close.
Expand Down
88 changes: 88 additions & 0 deletions clients/githubrepo/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2021 Security Scorecard Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package githubrepo

import (
"context"
"errors"
"fmt"
"strings"

"github.com/google/go-github/v38/github"

"github.com/ossf/scorecard/v2/clients"
)

var errEmptyQuery = errors.New("search query is empty")

type searchHandler struct {
ghClient *github.Client
ctx context.Context
owner string
repo string
}

func (handler *searchHandler) init(ctx context.Context, owner, repo string) {
handler.ctx = ctx
handler.owner = owner
handler.repo = repo
}

func (handler *searchHandler) search(request clients.SearchRequest) (clients.SearchResponse, error) {
query, err := handler.buildQuery(request)
if err != nil {
return clients.SearchResponse{}, fmt.Errorf("handler.buildQuery: %w", err)
}

resp, _, err := handler.ghClient.Search.Code(handler.ctx, query, &github.SearchOptions{})
if err != nil {
return clients.SearchResponse{}, fmt.Errorf("Search.Code: %w", err)
}
return searchResponseFrom(resp), nil
}

func (handler *searchHandler) buildQuery(request clients.SearchRequest) (string, error) {
if request.Query == "" {
return "", fmt.Errorf("%w", errEmptyQuery)
}
var queryBuilder strings.Builder
if _, err := queryBuilder.WriteString(
azeemshaikh38 marked this conversation as resolved.
Show resolved Hide resolved
fmt.Sprintf("%s repo:%s/%s", request.Query, handler.owner, handler.repo)); err != nil {
return "", fmt.Errorf("WriteString: %w", err)
}
if request.Filename != "" {
if _, err := queryBuilder.WriteString(
fmt.Sprintf(" in:file filename:%s", request.Filename)); err != nil {
return "", fmt.Errorf("WriteString: %w", err)
}
}
if request.Path != "" {
if _, err := queryBuilder.WriteString(fmt.Sprintf(" path:%s", request.Path)); err != nil {
return "", fmt.Errorf("WriteString: %w", err)
}
}
return queryBuilder.String(), nil
}

func searchResponseFrom(resp *github.CodeSearchResult) clients.SearchResponse {
var ret clients.SearchResponse
ret.Hits = resp.GetTotal()
for _, result := range resp.CodeResults {
ret.Results = append(ret.Results, clients.SearchResult{
Path: result.GetPath(),
})
}
return ret
}
108 changes: 108 additions & 0 deletions clients/githubrepo/search_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2021 Security Scorecard Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package githubrepo

import (
"errors"
"testing"

"github.com/ossf/scorecard/v2/clients"
)

func TestBuildQuery(t *testing.T) {
t.Parallel()
testcases := []struct {
searchReq clients.SearchRequest
expectedErrType error
name string
owner string
repo string
expectedQuery string
hasError bool
}{
{
name: "Basic",
owner: "testowner",
repo: "testrepo",
searchReq: clients.SearchRequest{
Query: "testquery",
},
expectedQuery: "testquery repo:testowner/testrepo",
},
{
name: "EmptyQuery",
owner: "testowner",
repo: "testrepo",
searchReq: clients.SearchRequest{},
hasError: true,
expectedErrType: errEmptyQuery,
},
{
name: "WithFilename",
owner: "testowner",
repo: "testrepo",
searchReq: clients.SearchRequest{
Query: "testquery",
Filename: "filename1.txt",
},
expectedQuery: "testquery repo:testowner/testrepo in:file filename:filename1.txt",
},
{
name: "WithPath",
owner: "testowner",
repo: "testrepo",
searchReq: clients.SearchRequest{
Query: "testquery",
Path: "dir1/file1.txt",
},
expectedQuery: "testquery repo:testowner/testrepo path:dir1/file1.txt",
},
{
name: "WithFilenameAndPath",
owner: "testowner",
repo: "testrepo",
searchReq: clients.SearchRequest{
Query: "testquery",
Filename: "filename1.txt",
Path: "dir1/dir2",
},
expectedQuery: "testquery repo:testowner/testrepo in:file filename:filename1.txt path:dir1/dir2",
},
}

for _, testcase := range testcases {
testcase := testcase
t.Run(testcase.name, func(t *testing.T) {
t.Parallel()

handler := searchHandler{
owner: testcase.owner,
repo: testcase.repo,
}

query, err := handler.buildQuery(testcase.searchReq)
if !testcase.hasError && err != nil {
t.Fatalf("expected - no error, got: %v", err)
}
if testcase.hasError && !errors.Is(err, testcase.expectedErrType) {
t.Fatalf("expectedErrType - %v, got - %v",
testcase.expectedErrType, err)
} else if query != testcase.expectedQuery {
t.Fatalf("expectedQuery - %s, got - %s",
testcase.expectedQuery, query)
}
})
}
}
2 changes: 1 addition & 1 deletion clients/repo_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@ type RepoClient interface {
ListCommits() ([]Commit, error)
ListReleases() ([]Release, error)
ListContributors() ([]Contributor, error)
Search(request SearchRequest) (SearchResult, error)
Search(request SearchRequest) (SearchResponse, error)
Close() error
}
14 changes: 11 additions & 3 deletions clients/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,21 @@
package clients

// SearchRequest queries a repo for `Query`.
// If `Filename` is provided, only files with matching path is queried.
// If `Filename` is provided, only matching filenames are queried.
// If `Path` is provided, only files with matching paths are queried.
type SearchRequest struct {
Query string
Filename string
Path string
}

// SearchResult returns the results from a search request on a repo.
// SearchResponse returns the results from a search request on a repo.
type SearchResponse struct {
Results []SearchResult
Hits int
}

// SearchResult represents a matching result from the search query.
type SearchResult struct {
Hits int
Path string
}