Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ require (
github.com/aws/aws-lambda-go v1.30.0
github.com/aws/aws-sdk-go v1.43.45
github.com/paulvollmer/dependabot-config-go v0.1.1
gopkg.in/yaml.v2 v2.4.0
github.com/sirupsen/logrus v1.8.1
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gotest.tools v2.2.0+incompatible
)

require (
Expand All @@ -21,6 +22,7 @@ require (
github.com/goccy/go-json v0.9.7 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.7 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.0 // indirect
Expand All @@ -32,13 +34,13 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect
golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

require (
Expand All @@ -47,7 +49,7 @@ require (
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/go-containerregistry v0.8.0
github.com/google/go-github/v40 v40.0.0
github.com/jarcoal/httpmock v1.1.0
github.com/jarcoal/httpmock v1.4.0
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/lestrrat-go/jwx v1.2.25
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -850,8 +850,8 @@ github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6t
github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw=
github.com/jaguilar/vt100 v0.0.0-20150826170717-2703a27b14ea/go.mod h1:QMdK4dGB3YhEW2BmA1wgGpPYI3HZy/5gD705PXKUVSg=
github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jarcoal/httpmock v1.1.0 h1:F47ChZj1Y2zFsCXxNkBPwNNKnAyOATcdQibk0qEdVCE=
github.com/jarcoal/httpmock v1.1.0/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k=
github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a/go.mod h1:xRskid8CManxVta/ALEhJha/pweKBaVG6fWgc0yH25s=
github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
Expand Down Expand Up @@ -973,6 +973,8 @@ github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb44
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
Expand Down
66 changes: 66 additions & 0 deletions remediation/workflow/maintainedactions/getlatestrelease.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package maintainedactions

import (
"context"
"fmt"
"os"
"strings"

"github.com/google/go-github/v40/github"
"golang.org/x/oauth2"
)

type Release struct {
TagName string `json:"tag_name"`
}

func getMajorVersion(version string) string {
hasVPrefix := strings.HasPrefix(version, "v")
version = strings.TrimPrefix(version, "v")
parts := strings.Split(version, ".")
if len(parts) > 0 {
if hasVPrefix {
return "v" + parts[0]
}
return parts[0]
}
if hasVPrefix {
return "v" + version
}
return version
}

func GetLatestRelease(ownerRepo string) (string, error) {
splitOnSlash := strings.Split(ownerRepo, "/")
if len(splitOnSlash) != 2 {
return "", fmt.Errorf("invalid owner/repo format: %s", ownerRepo)
}
owner := splitOnSlash[0]
repo := splitOnSlash[1]

ctx := context.Background()

// First try without token
client := github.NewClient(nil)
release, _, err := client.Repositories.GetLatestRelease(ctx, owner, repo)
if err != nil {
// If failed, try with token
token := os.Getenv("PAT")
if token == "" {
return "", fmt.Errorf("failed to get latest release and no GITHUB_TOKEN available: %w", err)
}

ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
tc := oauth2.NewClient(ctx, ts)
client = github.NewClient(tc)

release, _, err = client.Repositories.GetLatestRelease(ctx, owner, repo)
if err != nil {
return "", fmt.Errorf("failed to get latest release with token: %w", err)
}
}

return getMajorVersion(release.GetTagName()), nil
}
143 changes: 143 additions & 0 deletions remediation/workflow/maintainedactions/maintainedActions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package maintainedactions

import (
"encoding/json"
"fmt"
"io/ioutil"
"strings"

"github.com/step-security/secure-repo/remediation/workflow/metadata"
"github.com/step-security/secure-repo/remediation/workflow/permissions"
"gopkg.in/yaml.v3"
)

// Action represents a GitHub Action in the maintained actions list
type Action struct {
Name string `json:"name"`
Description string `json:"description"`
ForkedFrom struct {
Name string `json:"name"`
} `json:"forkedFrom"`
Score int `json:"score"`
Image string `json:"image"`
}

type replacement struct {
jobName string
stepIdx int
newAction string
originalAction string
latestVersion string
}

// LoadMaintainedActions loads the maintained actions from the JSON file
func LoadMaintainedActions(jsonPath string) (map[string]string, error) {
// Read the JSON file
data, err := ioutil.ReadFile(jsonPath)
if err != nil {
return nil, fmt.Errorf("failed to read maintained actions file: %v", err)
}

// Parse the JSON
var actions []Action
if err := json.Unmarshal(data, &actions); err != nil {
return nil, fmt.Errorf("failed to parse maintained actions JSON: %v", err)
}

// Create a map of original actions to their Step Security replacements
actionMap := make(map[string]string)
for _, action := range actions {
if action.ForkedFrom.Name != "" {
actionMap[action.ForkedFrom.Name] = action.Name
}
}

return actionMap, nil
}

// ReplaceActions replaces original actions with Step Security actions in a workflow
func ReplaceActions(inputYaml string, customerMaintainedActions map[string]string) (string, bool, error) {
workflow := metadata.Workflow{}
updated := false

actionMap := customerMaintainedActions

err := yaml.Unmarshal([]byte(inputYaml), &workflow)
if err != nil {
return "", updated, fmt.Errorf("unable to parse yaml: %v", err)
}

// Step 1: Check if anything needs to be replaced

var replacements []replacement

for jobName, job := range workflow.Jobs {
if metadata.IsCallingReusableWorkflow(job) {
continue
}
for stepIdx, step := range job.Steps {
// fmt.Println("step ", step.Uses)
actionName := strings.Split(step.Uses, "@")[0]
if newAction, ok := actionMap[actionName]; ok {
latestVersion, err := GetLatestRelease(newAction)
if err != nil {
return "", updated, fmt.Errorf("unable to get latest release: %v", err)
}
replacements = append(replacements, replacement{
jobName: jobName,
stepIdx: stepIdx,
newAction: newAction,
originalAction: step.Uses,
latestVersion: latestVersion,
})
}
}
}
if len(replacements) == 0 {
// No changes needed
return inputYaml, false, nil
}

// Step 2: Now modify the YAML lines manually
t := yaml.Node{}
err = yaml.Unmarshal([]byte(inputYaml), &t)
if err != nil {
return "", updated, fmt.Errorf("unable to parse yaml: %v", err)
}

inputLines := strings.Split(inputYaml, "\n")
inputLines, updated = replaceAction(&t, inputLines, replacements, updated)

output := strings.Join(inputLines, "\n")

return output, updated, nil
}

func replaceAction(t *yaml.Node, inputLines []string, replacements []replacement, updated bool) ([]string, bool) {
for _, r := range replacements {
jobsNode := permissions.IterateNode(t, "jobs", "!!map", 0)
jobNode := permissions.IterateNode(jobsNode, r.jobName, "!!map", 0)
stepsNode := permissions.IterateNode(jobNode, "steps", "!!seq", 0)
if stepsNode == nil {
continue
}

// Now get the specific step
stepNode := stepsNode.Content[r.stepIdx]
usesNode := permissions.IterateNode(stepNode, "uses", "!!str", 0)
if usesNode == nil {
continue
}

lineNum := usesNode.Line - 1 // 0-based indexing
columnNum := usesNode.Column - 1

// Replace the line
oldLine := inputLines[lineNum]
prefix := oldLine[:columnNum]
inputLines[lineNum] = prefix + r.newAction + "@" + r.latestVersion
updated = true

}
return inputLines, updated
}
Loading