Skip to content

Commit 3298dbf

Browse files
add immutable action check
1 parent 929021e commit 3298dbf

File tree

3 files changed

+165
-1
lines changed

3 files changed

+165
-1
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package pin
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"regexp"
7+
"strings"
8+
9+
"github.com/google/go-containerregistry/pkg/name"
10+
"github.com/google/go-containerregistry/pkg/v1/remote"
11+
"github.com/sirupsen/logrus"
12+
)
13+
14+
var (
15+
githubImmutableActionArtifactType = "application/vnd.github.actions.package.v1+json"
16+
tagRegex = regexp.MustCompile(`v[0-9]+\.[0-9]+\.[0-9]+$`)
17+
)
18+
19+
type ociManifest struct {
20+
ArtifactType string `json:"artifactType"`
21+
}
22+
23+
// isImmutableAction checks if the action is an immutable action or not
24+
// It queries the OCI manifest for the action and checks if the artifact type is "application/vnd.github.actions.package.v1+json"
25+
//
26+
// Example usage:
27+
//
28+
// # Immutable action (returns true)
29+
// isImmutableAction("actions/[email protected]")
30+
//
31+
// # Non-Immutable action (returns false)
32+
// isImmutableAction("actions/[email protected]")
33+
//
34+
// REF - https://github.com/actions/publish-immutable-action/issues/216#issuecomment-2549914784
35+
func IsImmutableAction(action string) bool {
36+
37+
artifactType, err := getOCIImageArtifactTypeForGhAction(action)
38+
if err != nil {
39+
// log the error
40+
logrus.WithFields(logrus.Fields{"action": action}).WithError(err).Error("error in getting OCI manifest for image")
41+
return false
42+
}
43+
44+
if artifactType == githubImmutableActionArtifactType {
45+
return true
46+
}
47+
return false
48+
49+
}
50+
51+
// getOCIImageArtifactTypeForGhAction retrieves the artifact type from a GitHub Action's OCI manifest.
52+
// This function is used to determine if an action is immutable by checking its artifact type.
53+
//
54+
// Example usage:
55+
//
56+
// # Immutable action (returns "application/vnd.github.actions.package.v1+json", nil)
57+
// artifactType, err := getOCIImageArtifactTypeForGhAction("actions/[email protected]")
58+
//
59+
// Returns:
60+
// - artifactType: The artifact type string from the OCI manifest
61+
// - error: An error if the action format is invalid or if there's a problem retrieving the manifest
62+
func getOCIImageArtifactTypeForGhAction(action string) (string, error) {
63+
64+
// Split the action into parts (e.g., "actions/checkout@v2" -> ["actions/checkout", "v2"])
65+
parts := strings.Split(action, "@")
66+
if len(parts) != 2 {
67+
return "", fmt.Errorf("invalid action format")
68+
}
69+
70+
// convert v1.x.x to 1.x.x which is
71+
// use regexp to match tag version format and replace v in prefix
72+
// as immutable actions image tag is in format 1.x.x (without v prefix)
73+
// REF - https://github.com/actions/publish-immutable-action/issues/216#issuecomment-2549914784
74+
if tagRegex.MatchString(parts[1]) {
75+
// v1.x.x -> 1.x.x
76+
parts[1] = strings.TrimPrefix(parts[1], "v")
77+
}
78+
79+
// Convert GitHub action to GHCR image reference using proper OCI reference format
80+
image := fmt.Sprintf("ghcr.io/%s:%s", parts[0], parts[1])
81+
imageManifest, err := getOCIManifestForImage(image)
82+
if err != nil {
83+
return "", err
84+
}
85+
86+
var ociManifest ociManifest
87+
err = json.Unmarshal([]byte(imageManifest), &ociManifest)
88+
if err != nil {
89+
return "", err
90+
}
91+
return ociManifest.ArtifactType, nil
92+
}
93+
94+
// getOCIManifestForImage retrieves the artifact type from the OCI image manifest
95+
func getOCIManifestForImage(imageRef string) (string, error) {
96+
97+
// Parse the image reference
98+
ref, err := name.ParseReference(imageRef)
99+
if err != nil {
100+
return "", fmt.Errorf("error parsing reference: %v", err)
101+
}
102+
103+
// Get the image manifest
104+
desc, err := remote.Get(ref)
105+
if err != nil {
106+
return "", fmt.Errorf("error getting manifest: %v", err)
107+
}
108+
109+
return string(desc.Manifest), nil
110+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package pin
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func Test_isImmutableAction(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
action string
11+
want bool
12+
}{
13+
{
14+
name: "immutable action - 1",
15+
action: "actions/[email protected]",
16+
want: true,
17+
},
18+
{
19+
name: "immutable action - 2",
20+
action: "step-security/[email protected]",
21+
want: true,
22+
},
23+
{
24+
name: "non immutable action(valid action)",
25+
action: "sailikhith-stepsecurity/[email protected]",
26+
want: false,
27+
},
28+
{
29+
name: "non immutable action(invalid action)",
30+
action: "sailikhith-stepsecurity/[email protected]",
31+
want: false,
32+
},
33+
{
34+
name: " action with release tag doesn't exist",
35+
action: "actions/[email protected]",
36+
want: false,
37+
},
38+
{
39+
name: "invalid action format",
40+
action: "invalid-format",
41+
want: false,
42+
},
43+
}
44+
45+
for _, tt := range tests {
46+
t.Run(tt.name, func(t *testing.T) {
47+
48+
got := IsImmutableAction(tt.action)
49+
if got != tt.want {
50+
t.Errorf("isImmutableAction() = %v, want %v", got, tt.want)
51+
}
52+
})
53+
}
54+
}

remediation/workflow/pin/pinactions.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func PinAction(action, inputYaml string) (string, bool) {
4343
return inputYaml, updated // Cannot pin local actions and docker actions
4444
}
4545

46-
if isAbsolute(action) {
46+
if isAbsolute(action) || IsImmutableAction(action) {
4747
return inputYaml, updated
4848
}
4949
leftOfAt := strings.Split(action, "@")

0 commit comments

Comments
 (0)