Skip to content

Commit 7b9f651

Browse files
Merge pull request #2523 from step-security/maintained-actions6
Add support for maintained actions
2 parents 7c5d265 + afa2cf4 commit 7b9f651

19 files changed

+1230
-27
lines changed

go.mod

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ require (
77
github.com/aws/aws-lambda-go v1.30.0
88
github.com/aws/aws-sdk-go v1.43.45
99
github.com/paulvollmer/dependabot-config-go v0.1.1
10-
gopkg.in/yaml.v2 v2.4.0
10+
github.com/sirupsen/logrus v1.8.1
1111
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
12+
gotest.tools v2.2.0+incompatible
1213
)
1314

1415
require (
@@ -21,6 +22,7 @@ require (
2122
github.com/goccy/go-json v0.9.7 // indirect
2223
github.com/gogo/protobuf v1.3.2 // indirect
2324
github.com/golang/protobuf v1.5.2 // indirect
25+
github.com/google/go-cmp v0.5.7 // indirect
2426
github.com/google/go-querystring v1.1.0 // indirect
2527
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
2628
github.com/lestrrat-go/blackmagic v1.0.0 // indirect
@@ -32,13 +34,13 @@ require (
3234
github.com/opencontainers/go-digest v1.0.0 // indirect
3335
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
3436
github.com/pkg/errors v0.9.1 // indirect
35-
github.com/sirupsen/logrus v1.8.1 // indirect
3637
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect
3738
golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 // indirect
3839
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
3940
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
4041
google.golang.org/appengine v1.6.7 // indirect
4142
google.golang.org/protobuf v1.28.0 // indirect
43+
gopkg.in/yaml.v2 v2.4.0 // indirect
4244
)
4345

4446
require (
@@ -47,7 +49,7 @@ require (
4749
github.com/golang-jwt/jwt v3.2.2+incompatible
4850
github.com/google/go-containerregistry v0.8.0
4951
github.com/google/go-github/v40 v40.0.0
50-
github.com/jarcoal/httpmock v1.1.0
52+
github.com/jarcoal/httpmock v1.4.0
5153
github.com/jmespath/go-jmespath v0.4.0 // indirect
5254
github.com/lestrrat-go/jwx v1.2.25
5355
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -850,8 +850,8 @@ github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6t
850850
github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw=
851851
github.com/jaguilar/vt100 v0.0.0-20150826170717-2703a27b14ea/go.mod h1:QMdK4dGB3YhEW2BmA1wgGpPYI3HZy/5gD705PXKUVSg=
852852
github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
853-
github.com/jarcoal/httpmock v1.1.0 h1:F47ChZj1Y2zFsCXxNkBPwNNKnAyOATcdQibk0qEdVCE=
854-
github.com/jarcoal/httpmock v1.1.0/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
853+
github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k=
854+
github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
855855
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
856856
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a/go.mod h1:xRskid8CManxVta/ALEhJha/pweKBaVG6fWgc0yH25s=
857857
github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
@@ -973,6 +973,8 @@ github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb44
973973
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
974974
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
975975
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
976+
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
977+
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
976978
github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
977979
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
978980
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package maintainedactions
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
"github.com/google/go-github/v40/github"
10+
"golang.org/x/oauth2"
11+
)
12+
13+
type Release struct {
14+
TagName string `json:"tag_name"`
15+
}
16+
17+
func getMajorVersion(version string) string {
18+
hasVPrefix := strings.HasPrefix(version, "v")
19+
version = strings.TrimPrefix(version, "v")
20+
parts := strings.Split(version, ".")
21+
if len(parts) > 0 {
22+
if hasVPrefix {
23+
return "v" + parts[0]
24+
}
25+
return parts[0]
26+
}
27+
if hasVPrefix {
28+
return "v" + version
29+
}
30+
return version
31+
}
32+
33+
func GetLatestRelease(ownerRepo string) (string, error) {
34+
splitOnSlash := strings.Split(ownerRepo, "/")
35+
if len(splitOnSlash) != 2 {
36+
return "", fmt.Errorf("invalid owner/repo format: %s", ownerRepo)
37+
}
38+
owner := splitOnSlash[0]
39+
repo := splitOnSlash[1]
40+
41+
ctx := context.Background()
42+
43+
// First try without token
44+
client := github.NewClient(nil)
45+
release, _, err := client.Repositories.GetLatestRelease(ctx, owner, repo)
46+
if err != nil {
47+
// If failed, try with token
48+
token := os.Getenv("PAT")
49+
if token == "" {
50+
return "", fmt.Errorf("failed to get latest release and no GITHUB_TOKEN available: %w", err)
51+
}
52+
53+
ts := oauth2.StaticTokenSource(
54+
&oauth2.Token{AccessToken: token},
55+
)
56+
tc := oauth2.NewClient(ctx, ts)
57+
client = github.NewClient(tc)
58+
59+
release, _, err = client.Repositories.GetLatestRelease(ctx, owner, repo)
60+
if err != nil {
61+
return "", fmt.Errorf("failed to get latest release with token: %w", err)
62+
}
63+
}
64+
65+
return getMajorVersion(release.GetTagName()), nil
66+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package maintainedactions
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
7+
"strings"
8+
9+
"github.com/step-security/secure-repo/remediation/workflow/metadata"
10+
"github.com/step-security/secure-repo/remediation/workflow/permissions"
11+
"gopkg.in/yaml.v3"
12+
)
13+
14+
// Action represents a GitHub Action in the maintained actions list
15+
type Action struct {
16+
Name string `json:"name"`
17+
Description string `json:"description"`
18+
ForkedFrom struct {
19+
Name string `json:"name"`
20+
} `json:"forkedFrom"`
21+
Score int `json:"score"`
22+
Image string `json:"image"`
23+
}
24+
25+
type replacement struct {
26+
jobName string
27+
stepIdx int
28+
newAction string
29+
originalAction string
30+
latestVersion string
31+
}
32+
33+
// LoadMaintainedActions loads the maintained actions from the JSON file
34+
func LoadMaintainedActions(jsonPath string) (map[string]string, error) {
35+
// Read the JSON file
36+
data, err := ioutil.ReadFile(jsonPath)
37+
if err != nil {
38+
return nil, fmt.Errorf("failed to read maintained actions file: %v", err)
39+
}
40+
41+
// Parse the JSON
42+
var actions []Action
43+
if err := json.Unmarshal(data, &actions); err != nil {
44+
return nil, fmt.Errorf("failed to parse maintained actions JSON: %v", err)
45+
}
46+
47+
// Create a map of original actions to their Step Security replacements
48+
actionMap := make(map[string]string)
49+
for _, action := range actions {
50+
if action.ForkedFrom.Name != "" {
51+
actionMap[action.ForkedFrom.Name] = action.Name
52+
}
53+
}
54+
55+
return actionMap, nil
56+
}
57+
58+
// ReplaceActions replaces original actions with Step Security actions in a workflow
59+
func ReplaceActions(inputYaml string, customerMaintainedActions map[string]string) (string, bool, error) {
60+
workflow := metadata.Workflow{}
61+
updated := false
62+
63+
actionMap := customerMaintainedActions
64+
65+
err := yaml.Unmarshal([]byte(inputYaml), &workflow)
66+
if err != nil {
67+
return "", updated, fmt.Errorf("unable to parse yaml: %v", err)
68+
}
69+
70+
// Step 1: Check if anything needs to be replaced
71+
72+
var replacements []replacement
73+
74+
for jobName, job := range workflow.Jobs {
75+
if metadata.IsCallingReusableWorkflow(job) {
76+
continue
77+
}
78+
for stepIdx, step := range job.Steps {
79+
// fmt.Println("step ", step.Uses)
80+
actionName := strings.Split(step.Uses, "@")[0]
81+
if newAction, ok := actionMap[actionName]; ok {
82+
latestVersion, err := GetLatestRelease(newAction)
83+
if err != nil {
84+
return "", updated, fmt.Errorf("unable to get latest release: %v", err)
85+
}
86+
replacements = append(replacements, replacement{
87+
jobName: jobName,
88+
stepIdx: stepIdx,
89+
newAction: newAction,
90+
originalAction: step.Uses,
91+
latestVersion: latestVersion,
92+
})
93+
}
94+
}
95+
}
96+
if len(replacements) == 0 {
97+
// No changes needed
98+
return inputYaml, false, nil
99+
}
100+
101+
// Step 2: Now modify the YAML lines manually
102+
t := yaml.Node{}
103+
err = yaml.Unmarshal([]byte(inputYaml), &t)
104+
if err != nil {
105+
return "", updated, fmt.Errorf("unable to parse yaml: %v", err)
106+
}
107+
108+
inputLines := strings.Split(inputYaml, "\n")
109+
inputLines, updated = replaceAction(&t, inputLines, replacements, updated)
110+
111+
output := strings.Join(inputLines, "\n")
112+
113+
return output, updated, nil
114+
}
115+
116+
func replaceAction(t *yaml.Node, inputLines []string, replacements []replacement, updated bool) ([]string, bool) {
117+
for _, r := range replacements {
118+
jobsNode := permissions.IterateNode(t, "jobs", "!!map", 0)
119+
jobNode := permissions.IterateNode(jobsNode, r.jobName, "!!map", 0)
120+
stepsNode := permissions.IterateNode(jobNode, "steps", "!!seq", 0)
121+
if stepsNode == nil {
122+
continue
123+
}
124+
125+
// Now get the specific step
126+
stepNode := stepsNode.Content[r.stepIdx]
127+
usesNode := permissions.IterateNode(stepNode, "uses", "!!str", 0)
128+
if usesNode == nil {
129+
continue
130+
}
131+
132+
lineNum := usesNode.Line - 1 // 0-based indexing
133+
columnNum := usesNode.Column - 1
134+
135+
// Replace the line
136+
oldLine := inputLines[lineNum]
137+
prefix := oldLine[:columnNum]
138+
inputLines[lineNum] = prefix + r.newAction + "@" + r.latestVersion
139+
updated = true
140+
141+
}
142+
return inputLines, updated
143+
}

0 commit comments

Comments
 (0)