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
85 changes: 58 additions & 27 deletions remediation/workflow/pin/pinactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,37 +80,67 @@ func PinAction(action, inputYaml string, exemptedActions []string, pinToImmutabl
return inputYaml, updated
}

pinnedAction := fmt.Sprintf("%s@%s # %s", leftOfAt[0], commitSHA, tagOrBranch)
// pinnedAction := fmt.Sprintf("%s@%s # %s", leftOfAt[0], commitSHA, tagOrBranch)
// build separately so we can quote only the ref, not the comment
pinnedRef := fmt.Sprintf("%s@%s", leftOfAt[0], commitSHA)
comment := fmt.Sprintf(" # %s", tagOrBranch)
fullPinned := pinnedRef + comment

// if the action with version is immutable, then pin the action with version instead of sha
pinnedActionWithVersion := fmt.Sprintf("%s@%s", leftOfAt[0], tagOrBranch)
if pinToImmutable && semanticTagRegex.MatchString(tagOrBranch) && IsImmutableAction(pinnedActionWithVersion) {
pinnedAction = pinnedActionWithVersion
}
// strings.ReplaceAll is not suitable here because it would incorrectly replace substrings
// For example, if we want to replace "actions/checkout@v1" to "actions/[email protected]", it would also incorrectly match and replace in "actions/[email protected]"
// making new string to "actions/[email protected]"
//
// Instead, we use a regex pattern that ensures we only replace complete action references:
// Pattern: (<action>@<version>)($|\s|"|')
// - Group 1 (<action>@<version>): Captures the exact action reference
// - Group 2 ($|\s|"|'): Captures the delimiter that follows (end of line, whitespace, or quotes)
//
// Examples:
// - "actions/[email protected]" - No match (no delimiter after v1)
// - "actions/checkout@v1 " - Matches (space delimiter)
// - "actions/checkout@v1"" - Matches (quote delimiter)
// - "actions/checkout@v1" - Matches (quote delimiter)
// - "actions/checkout@v1\n" - Matches (newline is considered whitespace \s)

actionRegex := regexp.MustCompile(`(` + regexp.QuoteMeta(action) + `)($|\s|"|')`)
inputYaml = actionRegex.ReplaceAllString(inputYaml, pinnedActionWithVersion+"$2")

inputYaml, _ = removePreviousActionComments(pinnedActionWithVersion, inputYaml)
return inputYaml, !strings.EqualFold(action, pinnedActionWithVersion)
}

updated = !strings.EqualFold(action, fullPinned)

// 1) Double-quoted form: "owner/repo@oldRef"
doubleQuotedPattern := `"` + regexp.QuoteMeta(action) + `"` + `($|\s|"|')`
doubleQuotedRe := regexp.MustCompile(doubleQuotedPattern)
inputYaml = doubleQuotedRe.ReplaceAllString(
inputYaml,
fmt.Sprintf(`"%s"%s$1`, pinnedRef, comment),
)
inputYaml, _ = removePreviousActionComments(fmt.Sprintf(`"%s"%s`, pinnedRef, comment), inputYaml)

// 2) Single-quoted form: 'owner/repo@oldRef'
singleQuotedPattern := `'` + regexp.QuoteMeta(action) + `'` + `($|\s|"|')`
singleQuotedRe := regexp.MustCompile(singleQuotedPattern)
inputYaml = singleQuotedRe.ReplaceAllString(
inputYaml,
fmt.Sprintf(`'%s'%s$1`, pinnedRef, comment),
)
inputYaml, _ = removePreviousActionComments(fmt.Sprintf(`'%s'%s`, pinnedRef, comment), inputYaml)

// 3) Unquoted form: owner/repo@oldRef
unqPattern := `\b` + regexp.QuoteMeta(action) + `\b` + `($|\s|"|')`
unqRe := regexp.MustCompile(unqPattern)
inputYaml = unqRe.ReplaceAllString(
inputYaml,
fullPinned+`$1`,
)
inputYaml, _ = removePreviousActionComments(fullPinned, inputYaml)

updated = !strings.EqualFold(action, pinnedAction)

// strings.ReplaceAll is not suitable here because it would incorrectly replace substrings
// For example, if we want to replace "actions/checkout@v1" to "actions/[email protected]", it would also incorrectly match and replace in "actions/[email protected]"
// making new string to "actions/[email protected]"
//
// Instead, we use a regex pattern that ensures we only replace complete action references:
// Pattern: (<action>@<version>)($|\s|"|')
// - Group 1 (<action>@<version>): Captures the exact action reference
// - Group 2 ($|\s|"|'): Captures the delimiter that follows (end of line, whitespace, or quotes)
//
// Examples:
// - "actions/[email protected]" - No match (no delimiter after v1)
// - "actions/checkout@v1 " - Matches (space delimiter)
// - "actions/checkout@v1"" - Matches (quote delimiter)
// - "actions/checkout@v1" - Matches (quote delimiter)
// - "actions/checkout@v1\n" - Matches (newline is considered whitespace \s)
actionRegex := regexp.MustCompile(`(` + regexp.QuoteMeta(action) + `)($|\s|"|')`)
inputYaml = actionRegex.ReplaceAllString(inputYaml, pinnedAction+"$2")
yamlWithPreviousActionCommentsRemoved, wasModified := removePreviousActionComments(pinnedAction, inputYaml)
if wasModified {
return yamlWithPreviousActionCommentsRemoved, updated
}
return inputYaml, updated
}

Expand All @@ -126,11 +156,12 @@ func removePreviousActionComments(pinnedAction, inputYaml string) (string, bool)
inputYaml = stringParts[0]
for idx := 1; idx < len(stringParts); idx++ {
trimmedString := strings.SplitN(stringParts[idx], "\n", 2)
inputYaml = inputYaml + pinnedAction
if len(trimmedString) > 1 {
if strings.Contains(trimmedString[0], "#") {
updated = true
}
inputYaml = inputYaml + pinnedAction + "\n" + trimmedString[1]
inputYaml = inputYaml + "\n" + trimmedString[1]
}
}
}
Expand Down
1 change: 1 addition & 0 deletions remediation/workflow/pin/pinactions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ func TestPinActions(t *testing.T) {
{fileName: "immutableaction-1.yml", wantUpdated: true, pinToImmutable: true},
{fileName: "exemptaction.yml", wantUpdated: true, exemptedActions: []string{"actions/checkout", "rohith/*"}, pinToImmutable: true},
{fileName: "donotpintoimmutable.yml", wantUpdated: true, pinToImmutable: false},
{fileName: "invertedcommas.yml", wantUpdated: true, pinToImmutable: false},
}
for _, tt := range tests {
input, err := ioutil.ReadFile(path.Join(inputDirectory, tt.fileName))
Expand Down
15 changes: 15 additions & 0 deletions testfiles/pinactions/input/invertedcommas.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: "close issue"

on:
push:

jobs:
closeissue:
runs-on: ubuntu-latest

steps:
- name: Close Issue
uses: "peter-evans/close-issue@v1"
with:
issue-number: 1
comment: Auto-closing issue
15 changes: 15 additions & 0 deletions testfiles/pinactions/output/invertedcommas.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: "close issue"

on:
push:

jobs:
closeissue:
runs-on: ubuntu-latest

steps:
- name: Close Issue
uses: "peter-evans/close-issue@a700eac5bf2a1c7a8cb6da0c13f93ed96fd53dbe" # v1.0.3
with:
issue-number: 1
comment: Auto-closing issue