Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
38009b2
First Iteration
gabrielsoltz Sep 16, 2025
373f1cb
switch-to-client
gabrielsoltz Sep 16, 2025
5142e77
implement-outputs
gabrielsoltz Sep 16, 2025
385e947
minor-improvements
gabrielsoltz Sep 16, 2025
d59a01a
tests-rg
gabrielsoltz Sep 16, 2025
05b3daa
Fix linter issues
gabrielsoltz Sep 17, 2025
0e828bf
add-license
gabrielsoltz Sep 17, 2025
c816410
add-license
gabrielsoltz Sep 17, 2025
32d7baa
fix linter
gabrielsoltz Sep 17, 2025
59153ba
Restore deleted comments
gabrielsoltz Sep 19, 2025
026d3f1
Restore comments
gabrielsoltz Sep 19, 2025
84d0861
Move org code to cmd/internal and reuse go-github v53
gabrielsoltz Sep 19, 2025
2fcc877
ParseOrgName support for URLs
gabrielsoltz Sep 19, 2025
a2fff13
Better Mocking
gabrielsoltz Sep 19, 2025
00b6f0f
add-license
gabrielsoltz Sep 20, 2025
1f92674
Adds new Chromium dependencies to cron scan config. (#4794)
renewitt Sep 18, 2025
9f95ef3
:seedling: Bump the gomod group across 2 directories with 4 updates (…
dependabot[bot] Sep 22, 2025
9e22309
:seedling: Bump the distroless group across 6 directories with 1 upda…
dependabot[bot] Sep 22, 2025
929cbb3
:seedling: Bump sigstore/cosign-installer in the github-actions group…
dependabot[bot] Sep 22, 2025
516998a
:book: add error clarification for github branch protection errors (#…
spencerschrock Sep 22, 2025
98bb812
:seedling: migrate tablewriter dependency to v1 new API (#4796)
spencerschrock Sep 22, 2025
cf94c68
:seedling: detect blocked user name and email in dangerous workflow (…
AdamKorcz Sep 23, 2025
9f23d97
:book: fix typo in a code comment (#4801)
deivid-rodriguez Sep 25, 2025
f6f94d8
:bug: parse docker build args when evaluating pinned containers (#4780)
spencerschrock Sep 29, 2025
890ce9d
:bug: add logic for pinned downloads across multiple commands (#4777)
spencerschrock Sep 29, 2025
3a9e4b6
:seedling: Bump the gomod group across 2 directories with 3 updates (…
dependabot[bot] Sep 29, 2025
33efb62
:seedling: Bump the github-actions group with 3 updates (#4804)
dependabot[bot] Sep 29, 2025
46ddbbd
chore: Add Hiero's hiero-cli (#4806)
jwagantall Oct 1, 2025
aca199d
MAINTAINERS: Add Adam Korczynski (AdamKorcz), ADA Logics (#4808)
justaugustus Oct 2, 2025
9783816
fix: fix dependencies typo (#4809)
martincostello Oct 3, 2025
590d77e
:seedling: Bump the gomod group across 2 directories with 8 updates (…
dependabot[bot] Oct 10, 2025
a56c1c7
chore: remove redundant code (#4810)
martincostello Oct 10, 2025
52193f8
Added key Qwen, Meta-Llama, and OSS GPT repositories (#4811)
mkdolan Oct 11, 2025
4cc0984
:seedling: Bump the golang group across 8 directories with 1 update (…
dependabot[bot] Oct 11, 2025
b864b9b
:seedling: Bump the github-actions group with 3 updates (#4812)
dependabot[bot] Oct 11, 2025
6dd40bd
:seedling: Bump the distroless group across 6 directories with 1 upda…
dependabot[bot] Oct 11, 2025
339a3d6
:seedling: cron: repair GitHub project list with excess path componen…
spencerschrock Oct 12, 2025
cdc4d61
Delete unused scheme case
gabrielsoltz Oct 16, 2025
776379c
migrate tablewriter dependency to v1 new API
gabrielsoltz Oct 16, 2025
e51c83d
better-flags-description
gabrielsoltz Oct 16, 2025
b60fd9d
better-flags-description
gabrielsoltz Oct 16, 2025
b601277
better-comment
gabrielsoltz Oct 16, 2025
5318471
Rely on ListOrgRepos
gabrielsoltz Oct 16, 2025
93f0f03
Add sawRuntimeErr for returning it
gabrielsoltz Oct 16, 2025
0df2c8d
Delete repoLabelFromURI and rely on uri
gabrielsoltz Oct 16, 2025
c1448d6
Delete unused checksList
gabrielsoltz Oct 16, 2025
07e8adc
Delete combined flag, instead use --format combined
gabrielsoltz Oct 16, 2025
538b3ee
fix conflict
gabrielsoltz Oct 16, 2025
feb0b0e
fix linter errors
gabrielsoltz Oct 16, 2025
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
127 changes: 127 additions & 0 deletions cmd/internal/org/org.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2025 OpenSSF 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 org

import (
"context"
"errors"
"fmt"
"net/http"
"strings"

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

"github.com/ossf/scorecard/v5/clients/githubrepo/roundtripper"
"github.com/ossf/scorecard/v5/log"
)

// ErrNilResponse indicates the GitHub API returned a nil response object.
var ErrNilResponse = errors.New("nil response from GitHub API")

// ListOrgRepos lists all non-archived repositories for a GitHub organization.
// The caller should provide an http.RoundTripper (rt). If rt is nil, the
// default transport will be created via roundtripper.NewTransport.
func ListOrgRepos(ctx context.Context, orgName string, rt http.RoundTripper) ([]string, error) {
// Parse org name if needed.
if len(orgName) > 0 {
if parsed := parseOrgName(orgName); parsed != "" {
orgName = parsed
}
}

// Use the centralized transport so we respect token rotation, GitHub App
// auth, rate limiting and instrumentation already implemented in
// clients/githubrepo/roundtripper.
logger := log.NewLogger(log.DefaultLevel)
if rt == nil {
rt = roundtripper.NewTransport(ctx, logger)
}

Check warning on line 50 in cmd/internal/org/org.go

View check run for this annotation

Codecov / codecov/patch

cmd/internal/org/org.go#L49-L50

Added lines #L49 - L50 were not covered by tests
httpClient := &http.Client{Transport: rt}
client := github.NewClient(httpClient)

opt := &github.RepositoryListByOrgOptions{
Type: "all",
}

var urls []string
for {
repos, resp, err := client.Repositories.ListByOrg(ctx, orgName, opt)
if err != nil {
return nil, fmt.Errorf("failed to list repos: %w", err)
}

Check warning on line 63 in cmd/internal/org/org.go

View check run for this annotation

Codecov / codecov/patch

cmd/internal/org/org.go#L62-L63

Added lines #L62 - L63 were not covered by tests

for _, r := range repos {
if r.GetArchived() {
continue
}
urls = append(urls, r.GetHTMLURL())
}

if resp == nil {
return nil, ErrNilResponse
}

Check warning on line 74 in cmd/internal/org/org.go

View check run for this annotation

Codecov / codecov/patch

cmd/internal/org/org.go#L73-L74

Added lines #L73 - L74 were not covered by tests
if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage

Check warning on line 78 in cmd/internal/org/org.go

View check run for this annotation

Codecov / codecov/patch

cmd/internal/org/org.go#L78

Added line #L78 was not covered by tests
}

return urls, nil
}

// parseOrgName extracts the GitHub organization from a supported input.
// Supported:
// - owner > owner
// - github.com/owner > owner
// - http://github.com/owner > owner
// - https://github.com/owner > owner
//
// Returns "" if no org can be parsed.
func parseOrgName(input string) string {
s := strings.TrimSpace(input)
if s == "" {
return ""
}

// Strip optional scheme.
switch {
case strings.HasPrefix(s, "https://"):
s = strings.TrimPrefix(s, "https://")
case strings.HasPrefix(s, "http://"):
s = strings.TrimPrefix(s, "http://")
}

// If it's exactly the host, there's no org.
if s == "github.com" {
return ""
}

Check warning on line 109 in cmd/internal/org/org.go

View check run for this annotation

Codecov / codecov/patch

cmd/internal/org/org.go#L108-L109

Added lines #L108 - L109 were not covered by tests

// Strip host prefix if present.
if after, ok := strings.CutPrefix(s, "github.com/"); ok {
s = after
}

// Keep only the first path segment (the org).
if i := strings.IndexByte(s, '/'); i >= 0 {
s = s[:i]
}

Check warning on line 119 in cmd/internal/org/org.go

View check run for this annotation

Codecov / codecov/patch

cmd/internal/org/org.go#L118-L119

Added lines #L118 - L119 were not covered by tests

// Basic sanity: org shouldn't contain dots (to avoid host-like values).
if s == "" || strings.Contains(s, ".") {
return ""
}

Check warning on line 124 in cmd/internal/org/org.go

View check run for this annotation

Codecov / codecov/patch

cmd/internal/org/org.go#L123-L124

Added lines #L123 - L124 were not covered by tests

return s
}
91 changes: 91 additions & 0 deletions cmd/internal/org/org_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2025 OpenSSF 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 org

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

func TestParseOrgName(t *testing.T) {
t.Parallel()
cases := []struct {
in string
want string
}{
{"http://github.com/owner", "owner"},
{"https://github.com/owner", "owner"},
{"github.com/owner", "owner"},
{"owner", "owner"},
{"", ""},
}
for _, c := range cases {
if got := parseOrgName(c.in); got != c.want {
t.Fatalf("parseOrgName(%q) = %q; want %q", c.in, got, c.want)
}
}
}

// Test ListOrgRepos handles pagination and filters archived repos.
func TestListOrgRepos_PaginationAndArchived(t *testing.T) {
t.Parallel()
// Single page: one archived repo and two active repos; expect active ones returned.
body := `[
{"html_url": "https://github.com/owner/repo1", "archived": true},
{"html_url": "https://github.com/owner/repo2", "archived": false},
{"html_url": "https://github.com/owner/repo3", "archived": false}
]`

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, err := w.Write([]byte(body)); err != nil {
t.Fatalf("failed to write response: %v", err)
}
}))
defer srv.Close()

// Override TransportFactory to redirect requests to our test server.
rt := roundTripperToServer(srv.URL)

repos, err := ListOrgRepos(context.Background(), "owner", rt)
if err != nil {
t.Fatalf("ListOrgRepos returned error: %v", err)
}
// Expect repo2 and repo3 (repo1 archived)
if len(repos) != 2 {
t.Fatalf("expected 2 repos, got %d: %v", len(repos), repos)
}
if !strings.Contains(repos[0], "repo2") || !strings.Contains(repos[1], "repo3") {
t.Fatalf("unexpected repos: %v", repos)
}
}

// roundTripperToServer returns an http.RoundTripper that rewrites requests
// to the given serverURL, keeping the path and query intact.
func roundTripperToServer(serverURL string) http.RoundTripper {
return http.RoundTripper(httpTransportFunc(func(req *http.Request) (*http.Response, error) {
// rewrite target
req.URL.Scheme = "http"
req.URL.Host = strings.TrimPrefix(serverURL, "http://")
return http.DefaultTransport.RoundTrip(req)
}))
}

// httpTransportFunc converts a function into an http.RoundTripper.
type httpTransportFunc func(*http.Request) (*http.Response, error)

func (f httpTransportFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
Loading
Loading