Skip to content
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
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