Skip to content

Commit 8fd04df

Browse files
Merge pull request step-security#2592 from step-security/fix/dependabot-config-bugs
Fix dependabot config bugs and improve test coverage
2 parents 89a16e1 + c9140ff commit 8fd04df

20 files changed

Lines changed: 655 additions & 35 deletions

remediation/dependabot/dependabotconfig.go

Lines changed: 94 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -141,35 +141,14 @@ func UpdateDependabotConfig(dependabotConfig string) (*UpdateDependabotConfigRes
141141
return response, nil
142142
}
143143

144-
// Using strings.Builder for efficient string concatenation
145-
var finalOutput strings.Builder
146-
finalOutput.WriteString(response.FinalOutput)
147-
148144
if updateDependabotConfigRequest.Content == "" {
145+
// Empty content: build from scratch using string concatenation.
149146
if len(updateDependabotConfigRequest.Ecosystems) == 0 {
150147
return response, nil
151148
}
149+
var finalOutput strings.Builder
152150
finalOutput.WriteString("version: 2\nupdates:")
153-
} else {
154-
if !strings.HasSuffix(response.FinalOutput, "\n") {
155-
finalOutput.WriteString("\n")
156-
}
157-
indentation, err = getIndentation(string(inputConfigFile))
158-
if err != nil {
159-
return nil, fmt.Errorf("failed to get indentation: %v", err)
160-
}
161-
}
162-
163-
for _, Update := range updateDependabotConfigRequest.Ecosystems {
164-
updateAlreadyExist := false
165-
for _, update := range cfg.Updates {
166-
if update.PackageEcosystem == Update.PackageEcosystem && (update.Directory == Update.Directory || update.Directory == Update.Directory+"/") {
167-
updateAlreadyExist = true
168-
break
169-
}
170-
}
171-
172-
if !updateAlreadyExist {
151+
for _, Update := range updateDependabotConfigRequest.Ecosystems {
173152
item := ExtendedUpdate{
174153
Update: dependabot.Update{
175154
PackageEcosystem: Update.PackageEcosystem,
@@ -184,18 +163,83 @@ func UpdateDependabotConfig(dependabotConfig string) (*UpdateDependabotConfigRes
184163
if err != nil {
185164
return nil, fmt.Errorf("failed to marshal update items: %v", err)
186165
}
187-
188166
data, err := addIndentation(string(addedItem), indentation)
189167
if err != nil {
190168
return nil, fmt.Errorf("failed to add indentation: %v", err)
191169
}
192170
finalOutput.WriteString(data)
193171
response.IsChanged = true
194172
}
195-
}
173+
response.FinalOutput = finalOutput.String()
174+
} else {
175+
// Non-empty content: insert new entries at the end of the updates section
176+
// so that sibling top-level keys like registries are preserved in place.
177+
indentation, err = getIndentation(string(inputConfigFile))
178+
if err != nil {
179+
return nil, fmt.Errorf("failed to get indentation: %v", err)
180+
}
181+
182+
var rootNode yaml.Node
183+
if err := yaml.Unmarshal(inputConfigFile, &rootNode); err != nil {
184+
return nil, fmt.Errorf("failed to parse yaml for insertion point: %v", err)
185+
}
186+
if len(rootNode.Content) == 0 {
187+
return nil, fmt.Errorf("failed to parse yaml: document is empty")
188+
}
189+
docNode := rootNode.Content[0]
190+
updatesNode := findMappingValue(docNode, "updates")
191+
if updatesNode == nil || updatesNode.Kind != yaml.SequenceNode {
192+
return nil, fmt.Errorf("missing or invalid 'updates' section in dependabot config")
193+
}
196194

197-
// Set FinalOutput to the built string
198-
response.FinalOutput = finalOutput.String()
195+
inputLines := strings.Split(response.FinalOutput, "\n")
196+
updatesLastLine := findLastLine(updatesNode)
197+
lineOffset := 0
198+
199+
for _, Update := range updateDependabotConfigRequest.Ecosystems {
200+
updateAlreadyExist := false
201+
for _, update := range cfg.Updates {
202+
if update.PackageEcosystem == Update.PackageEcosystem && (update.Directory == Update.Directory || update.Directory == Update.Directory+"/") {
203+
updateAlreadyExist = true
204+
break
205+
}
206+
}
207+
208+
if !updateAlreadyExist {
209+
item := ExtendedUpdate{
210+
Update: dependabot.Update{
211+
PackageEcosystem: Update.PackageEcosystem,
212+
Directory: Update.Directory,
213+
Schedule: dependabot.Schedule{Interval: Update.Interval},
214+
},
215+
Groups: Update.Groups,
216+
CoolDown: Update.CoolDown,
217+
}
218+
items := []ExtendedUpdate{item}
219+
addedItem, err := yaml.Marshal(items)
220+
if err != nil {
221+
return nil, fmt.Errorf("failed to marshal update items: %v", err)
222+
}
223+
data, err := addIndentation(string(addedItem), indentation)
224+
if err != nil {
225+
return nil, fmt.Errorf("failed to add indentation: %v", err)
226+
}
227+
228+
// Trim trailing newline to avoid double blank lines when content
229+
// follows after the updates section (e.g. registries block).
230+
dataLines := strings.Split(strings.TrimRight(data, "\n"), "\n")
231+
insertAt := updatesLastLine + lineOffset
232+
inputLines = insertAfterLine(inputLines, insertAt, dataLines)
233+
lineOffset += len(dataLines)
234+
response.IsChanged = true
235+
}
236+
}
237+
238+
response.FinalOutput = strings.Join(inputLines, "\n")
239+
if !strings.HasSuffix(response.FinalOutput, "\n") {
240+
response.FinalOutput += "\n"
241+
}
242+
}
199243

200244
return response, nil
201245
}
@@ -258,6 +302,16 @@ func replaceScalarOnLine(lines []string, lineIdx int, node *yaml.Node, newVal st
258302
line := lines[lineIdx]
259303
col := node.Column - 1
260304
end := col + len(node.Value)
305+
// When the original value is quoted, node.Column points to the opening quote
306+
// but node.Value is the unquoted content. Account for the surrounding quotes.
307+
if node.Style == yaml.DoubleQuotedStyle || node.Style == yaml.SingleQuotedStyle {
308+
end += 2 // skip both opening and closing quote
309+
if node.Style == yaml.DoubleQuotedStyle {
310+
newVal = "\"" + newVal + "\""
311+
} else {
312+
newVal = "'" + newVal + "'"
313+
}
314+
}
261315
if end > len(line) {
262316
end = len(line)
263317
}
@@ -765,13 +819,10 @@ func updateSubtractiveFields(content string, ecosystems []Ecosystem, cfg Config,
765819
return content, false, nil
766820
}
767821

768-
result := strings.Join(inputLines, "\n")
769-
770-
// Append ecosystems that were not found in existing config (additive).
822+
// Insert ecosystems that were not found in existing config at the end of the
823+
// updates section, so sibling top-level keys like registries stay in place.
824+
updatesLastLine := findLastLine(updatesNode) + lineOffset
771825
for _, eco := range toAdd {
772-
if !strings.HasSuffix(result, "\n") {
773-
result += "\n"
774-
}
775826
item := ExtendedUpdate{
776827
Update: dependabot.Update{
777828
PackageEcosystem: eco.PackageEcosystem,
@@ -792,9 +843,17 @@ func updateSubtractiveFields(content string, ecosystems []Ecosystem, cfg Config,
792843
if err != nil {
793844
return "", false, fmt.Errorf("failed to add indentation: %w", err)
794845
}
795-
result += data
846+
847+
dataLines := strings.Split(strings.TrimRight(data, "\n"), "\n")
848+
inputLines = insertAfterLine(inputLines, updatesLastLine, dataLines)
849+
updatesLastLine += len(dataLines)
850+
changed = true
796851
}
797852

853+
result := strings.Join(inputLines, "\n")
854+
if !strings.HasSuffix(result, "\n") {
855+
result += "\n"
856+
}
798857
return result, true, nil
799858
}
800859

0 commit comments

Comments
 (0)