Skip to content

Commit 810897b

Browse files
Merge pull request #2563 from step-security/shubham/remediation
feat: Implement runner label replacement functionality
2 parents c370517 + 56c304e commit 810897b

26 files changed

+667
-10
lines changed

remediation/workflow/addworkflow.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,4 @@ func AddWorkflow(name string, workflowParameters WorkflowParameters) (string, er
7070
} else {
7171
return "", fmt.Errorf("match for %s Workflow name not found", name)
7272
}
73-
}
73+
}

remediation/workflow/hardenrunner/addaction_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ func TestAddAction(t *testing.T) {
5757
func TestAddActionWithContainer(t *testing.T) {
5858
const inputDirectory = "../../../testfiles/addaction/input"
5959
const outputDirectory = "../../../testfiles/addaction/output"
60-
60+
6161
// Test container job with skipContainerJobs = true
6262
input, err := ioutil.ReadFile(path.Join(inputDirectory, "container-job.yml"))
6363
if err != nil {
6464
t.Fatalf("error reading test file")
6565
}
66-
66+
6767
// Test: Skip container jobs when skipContainerJobs = true
6868
got, gotUpdated, err := AddAction(string(input), "step-security/harden-runner@v2", false, false, true)
6969
if err != nil {

remediation/workflow/metadata/actionmetadata.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ type Runs struct {
4343
}
4444

4545
type Container struct {
46-
Image string `yaml:"image"`
47-
Options string `yaml:"options"`
48-
Env Env `yaml:"env"`
46+
Image string `yaml:"image"`
47+
Options string `yaml:"options"`
48+
Env Env `yaml:"env"`
4949
}
5050

5151
type Jobs map[string]Job

remediation/workflow/permissions/permissions.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type SecureWorkflowReponse struct {
2121
PinnedActions bool
2222
AddedHardenRunner bool
2323
AddedPermissions bool
24+
ReplacedRunnerLabels bool
2425
IncorrectYaml bool
2526
WorkflowFetchError bool
2627
JobErrors []JobError
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package runnerlabel
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/step-security/secure-repo/remediation/workflow/permissions"
8+
"gopkg.in/yaml.v3"
9+
)
10+
11+
// RunnerLabelMapping represents the replacement to be performed
12+
type RunnerLabelMapping struct {
13+
jobName string
14+
oldLabel string
15+
newLabel string
16+
lineNum int
17+
columnNum int
18+
isArray bool
19+
arrayIndex int
20+
}
21+
22+
// findRunsOnNode finds the runs-on node for a job, handling both string and array formats
23+
func findRunsOnNode(jobNode *yaml.Node) *yaml.Node {
24+
for i := 0; i < len(jobNode.Content); i += 2 {
25+
keyNode := jobNode.Content[i]
26+
if keyNode.Value == "runs-on" && i+1 < len(jobNode.Content) {
27+
return jobNode.Content[i+1]
28+
}
29+
}
30+
return nil
31+
}
32+
33+
// ReplaceRunnerLabels replaces runner labels in a workflow based on the provided label map
34+
// labelMap: map of old labels to new labels (e.g., "ubuntu-latest" -> "step-ubuntu-24")
35+
// Returns: updated YAML string, bool indicating if changes were made, error if any
36+
func ReplaceRunnerLabels(inputYaml string, labelMap map[string]string) (string, bool, error) {
37+
if len(labelMap) == 0 {
38+
return inputYaml, false, nil
39+
}
40+
41+
// Parse the YAML into a tree structure
42+
t := yaml.Node{}
43+
err := yaml.Unmarshal([]byte(inputYaml), &t)
44+
if err != nil {
45+
return "", false, fmt.Errorf("unable to parse yaml: %v", err)
46+
}
47+
48+
// Find all jobs node
49+
jobsNode := permissions.IterateNode(&t, "jobs", "!!map", 0)
50+
if jobsNode == nil {
51+
// No jobs found
52+
return inputYaml, false, nil
53+
}
54+
55+
// Collect all the replacements we need to make
56+
var replacements []RunnerLabelMapping
57+
58+
// Iterate through each job
59+
for i := 0; i < len(jobsNode.Content); i += 2 {
60+
jobNameNode := jobsNode.Content[i]
61+
jobNode := jobsNode.Content[i+1]
62+
63+
jobName := jobNameNode.Value
64+
65+
// Find the runs-on node for this job
66+
runsOnNode := findRunsOnNode(jobNode)
67+
if runsOnNode == nil {
68+
continue
69+
}
70+
71+
// Handle both string and array formats
72+
switch runsOnNode.Kind {
73+
case yaml.ScalarNode:
74+
// Single runner label
75+
oldLabel := runsOnNode.Value
76+
if newLabel, ok := labelMap[oldLabel]; ok {
77+
replacements = append(replacements, RunnerLabelMapping{
78+
jobName: jobName,
79+
oldLabel: oldLabel,
80+
newLabel: newLabel,
81+
lineNum: runsOnNode.Line - 1, // Convert to 0-based
82+
columnNum: runsOnNode.Column - 1,
83+
isArray: false,
84+
})
85+
}
86+
case yaml.SequenceNode:
87+
// Array of runner labels
88+
for idx, labelNode := range runsOnNode.Content {
89+
oldLabel := labelNode.Value
90+
if newLabel, ok := labelMap[oldLabel]; ok {
91+
replacements = append(replacements, RunnerLabelMapping{
92+
jobName: jobName,
93+
oldLabel: oldLabel,
94+
newLabel: newLabel,
95+
lineNum: labelNode.Line - 1, // Convert to 0-based
96+
columnNum: labelNode.Column - 1,
97+
isArray: true,
98+
arrayIndex: idx,
99+
})
100+
}
101+
}
102+
}
103+
}
104+
105+
if len(replacements) == 0 {
106+
// No changes needed
107+
return inputYaml, false, nil
108+
}
109+
110+
// Apply the replacements
111+
inputLines := strings.Split(inputYaml, "\n")
112+
updated := false
113+
114+
for _, r := range replacements {
115+
if r.lineNum >= len(inputLines) {
116+
continue
117+
}
118+
119+
oldLine := inputLines[r.lineNum]
120+
121+
// Get the prefix (indentation + key)
122+
prefix := oldLine[:r.columnNum]
123+
124+
// Replace the old label with the new one
125+
// We need to preserve any quotes, comments, etc.
126+
oldLineAfterColumn := oldLine[r.columnNum:]
127+
128+
// Simple replacement - replace the first occurrence of the old label
129+
newLineAfterColumn := strings.Replace(oldLineAfterColumn, r.oldLabel, r.newLabel, 1)
130+
131+
inputLines[r.lineNum] = prefix + newLineAfterColumn
132+
updated = true
133+
}
134+
135+
output := strings.Join(inputLines, "\n")
136+
return output, updated, nil
137+
}

0 commit comments

Comments
 (0)