Skip to content

Commit

Permalink
add immutable action check
Browse files Browse the repository at this point in the history
  • Loading branch information
sailikhith-stepsecurity committed Jan 16, 2025
1 parent d61982f commit 4b43059
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 1 deletion.
110 changes: 110 additions & 0 deletions remediation/workflow/pin/action_image_manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package pin

import (
"encoding/json"
"fmt"
"regexp"
"strings"

"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/sirupsen/logrus"
)

var (
githubImmutableActionArtifactType = "application/vnd.github.actions.package.v1+json"
tagRegex = regexp.MustCompile(`v[0-9]+\.[0-9]+\.[0-9]+$`)
)

type ociManifest struct {
ArtifactType string `json:"artifactType"`
}

// isImmutableAction checks if the action is an immutable action or not
// It queries the OCI manifest for the action and checks if the artifact type is "application/vnd.github.actions.package.v1+json"
//
// Example usage:
//
// # Immutable action (returns true)
// isImmutableAction("actions/checkout@v4.2.2")
//
// # Non-Immutable action (returns false)
// isImmutableAction("actions/checkout@v4.2.3")
//
// REF - https://github.com/actions/publish-immutable-action/issues/216#issuecomment-2549914784
func IsImmutableAction(action string) bool {

artifactType, err := getOCIImageArtifactTypeForGhAction(action)
if err != nil {
// log the error
logrus.WithFields(logrus.Fields{"action": action}).WithError(err).Error("error in getting OCI manifest for image")
return false
}

if artifactType == githubImmutableActionArtifactType {
return true
}
return false

}

// getOCIImageArtifactTypeForGhAction retrieves the artifact type from a GitHub Action's OCI manifest.
// This function is used to determine if an action is immutable by checking its artifact type.
//
// Example usage:
//
// # Immutable action (returns "application/vnd.github.actions.package.v1+json", nil)
// artifactType, err := getOCIImageArtifactTypeForGhAction("actions/checkout@v4.2.2")
//
// Returns:
// - artifactType: The artifact type string from the OCI manifest
// - error: An error if the action format is invalid or if there's a problem retrieving the manifest
func getOCIImageArtifactTypeForGhAction(action string) (string, error) {

// Split the action into parts (e.g., "actions/checkout@v2" -> ["actions/checkout", "v2"])
parts := strings.Split(action, "@")
if len(parts) != 2 {
return "", fmt.Errorf("invalid action format")
}

// convert v1.x.x to 1.x.x which is
// use regexp to match tag version format and replace v in prefix
// as immutable actions image tag is in format 1.x.x (without v prefix)
// REF - https://github.com/actions/publish-immutable-action/issues/216#issuecomment-2549914784
if tagRegex.MatchString(parts[1]) {
// v1.x.x -> 1.x.x
parts[1] = strings.TrimPrefix(parts[1], "v")
}

// Convert GitHub action to GHCR image reference using proper OCI reference format
image := fmt.Sprintf("ghcr.io/%s:%s", parts[0], parts[1])
imageManifest, err := getOCIManifestForImage(image)
if err != nil {
return "", err
}

var ociManifest ociManifest
err = json.Unmarshal([]byte(imageManifest), &ociManifest)
if err != nil {
return "", err
}
return ociManifest.ArtifactType, nil
}

// getOCIManifestForImage retrieves the artifact type from the OCI image manifest
func getOCIManifestForImage(imageRef string) (string, error) {

// Parse the image reference
ref, err := name.ParseReference(imageRef)
if err != nil {
return "", fmt.Errorf("error parsing reference: %v", err)
}

// Get the image manifest
desc, err := remote.Get(ref)
if err != nil {
return "", fmt.Errorf("error getting manifest: %v", err)
}

return string(desc.Manifest), nil
}
54 changes: 54 additions & 0 deletions remediation/workflow/pin/action_image_manifest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package pin

import (
"testing"
)

func Test_isImmutableAction(t *testing.T) {
tests := []struct {
name string
action string
want bool
}{
{
name: "immutable action - 1",
action: "actions/checkout@v4.2.2",
want: true,
},
{
name: "immutable action - 2",
action: "step-security/wait-for-secrets@v1.2.0",
want: true,
},
{
name: "non immutable action(valid action)",
action: "sailikhith-stepsecurity/hello-action@v1.0.2",
want: false,
},
{
name: "non immutable action(invalid action)",
action: "sailikhith-stepsecurity/no-such-action@v1.0.2",
want: false,
},
{
name: " action with release tag doesn't exist",
action: "actions/checkout@1.2.3",
want: false,
},
{
name: "invalid action format",
action: "invalid-format",
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

got := IsImmutableAction(tt.action)
if got != tt.want {
t.Errorf("isImmutableAction() = %v, want %v", got, tt.want)
}
})
}
}
2 changes: 1 addition & 1 deletion remediation/workflow/pin/pinactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func PinAction(action, inputYaml string) (string, bool) {
return inputYaml, updated // Cannot pin local actions and docker actions
}

if isAbsolute(action) {
if isAbsolute(action) || IsImmutableAction(action) {
return inputYaml, updated
}
leftOfAt := strings.Split(action, "@")
Expand Down

0 comments on commit 4b43059

Please sign in to comment.