Skip to content

Commit 772a098

Browse files
frjcompCopilot
andauthored
Add gitea variables command with comprehensive tests (#360)
* Add gitea variables command --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
1 parent 730a67f commit 772a098

6 files changed

Lines changed: 989 additions & 0 deletions

File tree

src/pipeleak/cmd/gitea/gitea.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package gitea
33
import (
44
"github.com/CompassSecurity/pipeleak/cmd/gitea/enum"
55
"github.com/CompassSecurity/pipeleak/cmd/gitea/scan"
6+
"github.com/CompassSecurity/pipeleak/cmd/gitea/variables"
67
"github.com/spf13/cobra"
78
)
89

@@ -16,6 +17,7 @@ func NewGiteaRootCmd() *cobra.Command {
1617

1718
giteaCmd.AddCommand(enum.NewEnumCmd())
1819
giteaCmd.AddCommand(scan.NewScanCmd())
20+
giteaCmd.AddCommand(variables.NewVariablesCommand())
1921

2022
return giteaCmd
2123
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package variables
2+
3+
import (
4+
"github.com/CompassSecurity/pipeleak/pkg/gitea/variables"
5+
"github.com/rs/zerolog/log"
6+
"github.com/spf13/cobra"
7+
)
8+
9+
func NewVariablesCommand() *cobra.Command {
10+
var (
11+
url string
12+
token string
13+
)
14+
15+
cmd := &cobra.Command{
16+
Use: "variables",
17+
Short: "List all Gitea Actions variables from groups and repositories",
18+
Long: `Fetches and logs all Actions variables from organizations and their repositories in Gitea.`,
19+
Run: func(cmd *cobra.Command, args []string) {
20+
config := variables.Config{
21+
URL: url,
22+
Token: token,
23+
}
24+
25+
if err := variables.ListAllVariables(config); err != nil {
26+
log.Fatal().Err(err).Msg("Failed to list variables")
27+
}
28+
},
29+
}
30+
31+
cmd.Flags().StringVarP(&url, "url", "u", "https://gitea.com", "Gitea server URL")
32+
cmd.Flags().StringVarP(&token, "token", "t", "", "Gitea access token (required)")
33+
34+
_ = cmd.MarkFlagRequired("token")
35+
36+
return cmd
37+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package variables
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestNewVariablesCommand(t *testing.T) {
10+
cmd := NewVariablesCommand()
11+
12+
assert.NotNil(t, cmd)
13+
assert.Equal(t, "variables", cmd.Use)
14+
assert.Contains(t, cmd.Short, "Actions variables")
15+
16+
urlFlag := cmd.Flags().Lookup("url")
17+
assert.NotNil(t, urlFlag)
18+
assert.Equal(t, "https://gitea.com", urlFlag.DefValue)
19+
20+
tokenFlag := cmd.Flags().Lookup("token")
21+
assert.NotNil(t, tokenFlag)
22+
assert.Equal(t, "", tokenFlag.DefValue)
23+
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package variables
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
8+
"code.gitea.io/sdk/gitea"
9+
"github.com/CompassSecurity/pipeleak/pkg/httpclient"
10+
"github.com/rs/zerolog/log"
11+
)
12+
13+
type Config struct {
14+
URL string
15+
Token string
16+
}
17+
18+
// clientContext holds both the SDK client and configuration needed for direct API calls
19+
type clientContext struct {
20+
client *gitea.Client
21+
token string
22+
url string
23+
}
24+
25+
func ListAllVariables(cfg Config) error {
26+
ctx, err := createClientContext(cfg)
27+
if err != nil {
28+
return fmt.Errorf("failed to create Gitea client: %w", err)
29+
}
30+
31+
// Fetch all repositories user has access to
32+
repos, err := fetchAllRepositories(ctx.client)
33+
if err != nil {
34+
return fmt.Errorf("failed to fetch repositories: %w", err)
35+
}
36+
37+
log.Info().Int("count", len(repos)).Msg("Found repositories")
38+
39+
// Fetch organization variables
40+
orgs, err := fetchOrganizations(ctx.client)
41+
if err != nil {
42+
return fmt.Errorf("failed to fetch organizations: %w", err)
43+
}
44+
45+
log.Info().Int("count", len(orgs)).Msg("Found organizations")
46+
47+
for _, org := range orgs {
48+
if err := fetchOrgVariables(ctx.client, org.UserName); err != nil {
49+
log.Warn().Err(err).Str("org", org.UserName).Msg("Failed to fetch organization variables")
50+
}
51+
}
52+
53+
// Fetch repository variables for all repos
54+
for _, repo := range repos {
55+
if err := fetchRepoVariables(ctx, repo.Owner.UserName, repo.Name); err != nil {
56+
log.Warn().Err(err).Str("owner", repo.Owner.UserName).Str("repo", repo.Name).Msg("Failed to fetch repository variables")
57+
}
58+
}
59+
60+
return nil
61+
}
62+
63+
func createClientContext(cfg Config) (*clientContext, error) {
64+
client, err := gitea.NewClient(cfg.URL, gitea.SetToken(cfg.Token))
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
return &clientContext{
70+
client: client,
71+
token: cfg.Token,
72+
url: cfg.URL,
73+
}, nil
74+
}
75+
76+
func fetchOrganizations(client *gitea.Client) ([]*gitea.Organization, error) {
77+
var allOrgs []*gitea.Organization
78+
page := 1
79+
pageSize := 50
80+
81+
for {
82+
orgs, resp, err := client.ListMyOrgs(gitea.ListOrgsOptions{
83+
ListOptions: gitea.ListOptions{
84+
Page: page,
85+
PageSize: pageSize,
86+
},
87+
})
88+
if err != nil {
89+
return nil, err
90+
}
91+
92+
allOrgs = append(allOrgs, orgs...)
93+
94+
if resp == nil || len(orgs) < pageSize {
95+
break
96+
}
97+
page++
98+
}
99+
100+
return allOrgs, nil
101+
}
102+
103+
func fetchAllRepositories(client *gitea.Client) ([]*gitea.Repository, error) {
104+
var allRepos []*gitea.Repository
105+
page := 1
106+
pageSize := 50
107+
108+
for {
109+
repos, resp, err := client.ListMyRepos(gitea.ListReposOptions{
110+
ListOptions: gitea.ListOptions{
111+
Page: page,
112+
PageSize: pageSize,
113+
},
114+
})
115+
if err != nil {
116+
return nil, err
117+
}
118+
119+
allRepos = append(allRepos, repos...)
120+
121+
if resp == nil || len(repos) < pageSize {
122+
break
123+
}
124+
page++
125+
}
126+
127+
return allRepos, nil
128+
}
129+
130+
func fetchOrgVariables(client *gitea.Client, orgName string) error {
131+
page := 1
132+
pageSize := 50
133+
134+
for {
135+
variables, resp, err := client.ListOrgActionVariable(orgName, gitea.ListOrgActionVariableOption{
136+
ListOptions: gitea.ListOptions{
137+
Page: page,
138+
PageSize: pageSize,
139+
},
140+
})
141+
if err != nil {
142+
return err
143+
}
144+
145+
for _, v := range variables {
146+
log.Info().
147+
Str("org", orgName).
148+
Str("variable_name", v.Name).
149+
Str("type", "organization").
150+
Str("value", v.Data).
151+
Msg("Variable")
152+
}
153+
154+
if resp == nil || len(variables) < pageSize {
155+
break
156+
}
157+
page++
158+
}
159+
160+
return nil
161+
}
162+
163+
// fetchRepoVariables fetches all variables for a specific repository using the Gitea API.
164+
// The SDK doesn't provide a ListRepoActionVariable method, so we use a direct API call.
165+
func fetchRepoVariables(ctx *clientContext, owner, repo string) error {
166+
page := 1
167+
pageSize := 50
168+
169+
for {
170+
variables, err := listRepoActionVariables(ctx, owner, repo, page, pageSize)
171+
if err != nil {
172+
return err
173+
}
174+
175+
for _, v := range variables {
176+
log.Info().
177+
Str("org", owner).
178+
Str("repo", repo).
179+
Str("variable_name", v.Name).
180+
Str("type", "repository").
181+
Str("value", v.Value).
182+
Msg("Variable")
183+
}
184+
185+
if len(variables) < pageSize {
186+
break
187+
}
188+
page++
189+
}
190+
191+
return nil
192+
}
193+
194+
// listRepoActionVariables calls the Gitea API directly to list repository action variables.
195+
// This implements the missing SDK method by making a direct HTTP request using pkg/httpclient.
196+
func listRepoActionVariables(ctx *clientContext, owner, repo string, page, pageSize int) ([]*gitea.RepoActionVariable, error) {
197+
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/actions/variables?page=%d&limit=%d", ctx.url, owner, repo, page, pageSize)
198+
199+
authHeaders := map[string]string{"Authorization": "token " + ctx.token}
200+
httpClient := httpclient.GetPipeleakHTTPClient("", nil, authHeaders)
201+
202+
resp, err := httpClient.Get(url)
203+
if err != nil {
204+
return nil, fmt.Errorf("failed to execute request: %w", err)
205+
}
206+
defer func() { _ = resp.Body.Close() }()
207+
208+
if resp.StatusCode != 200 {
209+
body, _ := io.ReadAll(resp.Body)
210+
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
211+
}
212+
213+
body, err := io.ReadAll(resp.Body)
214+
if err != nil {
215+
return nil, fmt.Errorf("failed to read response body: %w", err)
216+
}
217+
218+
var variables []*gitea.RepoActionVariable
219+
if err := json.Unmarshal(body, &variables); err != nil {
220+
return nil, fmt.Errorf("failed to parse response: %w", err)
221+
}
222+
223+
return variables, nil
224+
}

0 commit comments

Comments
 (0)