Skip to content

Commit a67b8ba

Browse files
Merge pull request #2578 from vamshi-stepsecurity/cherry-pick/extend-dpb-config
Cherry pick Support Cooldown and Groups for Dependabot config
2 parents ebdf2f9 + 2478f95 commit a67b8ba

23 files changed

+786
-21
lines changed

knowledge-base/actions/angular/dev-infra/github-actions/feature-request/action-security.yml

Lines changed: 0 additions & 2 deletions
This file was deleted.

knowledge-base/actions/angular/dev-infra/github-actions/pull-request-labeling/action-security.yml

Lines changed: 0 additions & 2 deletions
This file was deleted.

remediation/dependabot/dependabotconfig.go

Lines changed: 230 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dependabot
22

33
import (
44
"bufio"
5+
"bytes"
56
"encoding/json"
67
"errors"
78
"fmt"
@@ -22,11 +23,48 @@ type Ecosystem struct {
2223
PackageEcosystem string
2324
Directory string
2425
Interval string
26+
CoolDown *CoolDown `json:",omitempty"`
27+
Groups map[string]Group `json:",omitempty"`
2528
}
2629

2730
type UpdateDependabotConfigRequest struct {
28-
Ecosystems []Ecosystem
29-
Content string
31+
Ecosystems []Ecosystem
32+
Content string
33+
Subtractive bool
34+
}
35+
36+
// CoolDown represents the cooldown block, which the upstream dependabot package does not support.
37+
type CoolDown struct {
38+
DefaultDays int `yaml:"default-days,omitempty"`
39+
SemverMajorDays int `yaml:"semver-major-days,omitempty"`
40+
SemverMinorDays int `yaml:"semver-minor-days,omitempty"`
41+
SemverPatchDays int `yaml:"semver-patch-days,omitempty"`
42+
Include []string `yaml:"include,omitempty"`
43+
Exclude []string `yaml:"exclude,omitempty"`
44+
}
45+
46+
// Group represents a single entry in the groups block.
47+
type Group struct {
48+
AppliesTo string `yaml:"applies-to,omitempty"`
49+
Patterns []string `yaml:"patterns,omitempty"`
50+
ExcludePatterns []string `yaml:"exclude-patterns,omitempty"`
51+
DependencyType string `yaml:"dependency-type,omitempty"`
52+
UpdateTypes []string `yaml:"update-types,omitempty"`
53+
GroupBy string `yaml:"group-by,omitempty"`
54+
}
55+
56+
// Update embeds the upstream dependabot.Update inline so all its fields are preserved,
57+
// and extends it with the groups and cooldown blocks.
58+
type ExtendedUpdate struct {
59+
dependabot.Update `yaml:",inline"`
60+
Groups map[string]Group `yaml:"groups,omitempty"`
61+
CoolDown *CoolDown `yaml:"cooldown,omitempty"`
62+
}
63+
64+
// Config is the top-level dependabot config file structure backed by Update.
65+
type Config struct {
66+
Version int `yaml:"version"`
67+
Updates []ExtendedUpdate `yaml:"updates"`
3068
}
3169

3270
// getIndentation returns the indentation level of the first list found in a given YAML string.
@@ -70,8 +108,8 @@ func UpdateDependabotConfig(dependabotConfig string) (*UpdateDependabotConfigRes
70108
}
71109

72110
inputConfigFile := []byte(updateDependabotConfigRequest.Content)
73-
configMetadata := dependabot.New()
74-
err = configMetadata.Unmarshal(inputConfigFile)
111+
var cfg Config
112+
err = yaml.Unmarshal(inputConfigFile, &cfg)
75113
if err != nil {
76114
return nil, fmt.Errorf("failed to unmarshal dependabot config: %v", err)
77115
}
@@ -83,6 +121,24 @@ func UpdateDependabotConfig(dependabotConfig string) (*UpdateDependabotConfigRes
83121
response.OriginalInput = updateDependabotConfigRequest.Content
84122
response.IsChanged = false
85123

124+
// In subtractive mode, update only the specified fields of existing entries.
125+
if updateDependabotConfigRequest.Subtractive {
126+
if updateDependabotConfigRequest.Content == "" {
127+
return response, nil
128+
}
129+
subtractiveIndent, err := getIndentation(string(inputConfigFile))
130+
if err != nil {
131+
return nil, fmt.Errorf("failed to get indentation: %v", err)
132+
}
133+
newContent, changed, err := updateSubtractiveFields(response.FinalOutput, updateDependabotConfigRequest.Ecosystems, subtractiveIndent-1)
134+
if err != nil {
135+
return nil, fmt.Errorf("failed to apply subtractive update: %v", err)
136+
}
137+
response.FinalOutput = newContent
138+
response.IsChanged = changed
139+
return response, nil
140+
}
141+
86142
// Using strings.Builder for efficient string concatenation
87143
var finalOutput strings.Builder
88144
finalOutput.WriteString(response.FinalOutput)
@@ -104,20 +160,24 @@ func UpdateDependabotConfig(dependabotConfig string) (*UpdateDependabotConfigRes
104160

105161
for _, Update := range updateDependabotConfigRequest.Ecosystems {
106162
updateAlreadyExist := false
107-
for _, update := range configMetadata.Updates {
108-
if update.PackageEcosystem == Update.PackageEcosystem && update.Directory == Update.Directory {
163+
for _, update := range cfg.Updates {
164+
if update.PackageEcosystem == Update.PackageEcosystem && (update.Directory == Update.Directory || update.Directory == Update.Directory+"/") {
109165
updateAlreadyExist = true
110166
break
111167
}
112168
}
113169

114170
if !updateAlreadyExist {
115-
item := dependabot.Update{
116-
PackageEcosystem: Update.PackageEcosystem,
117-
Directory: Update.Directory,
118-
Schedule: dependabot.Schedule{Interval: Update.Interval},
171+
item := ExtendedUpdate{
172+
Update: dependabot.Update{
173+
PackageEcosystem: Update.PackageEcosystem,
174+
Directory: Update.Directory,
175+
Schedule: dependabot.Schedule{Interval: Update.Interval},
176+
},
177+
Groups: Update.Groups,
178+
CoolDown: Update.CoolDown,
119179
}
120-
items := []dependabot.Update{item}
180+
items := []ExtendedUpdate{item}
121181
addedItem, err := yaml.Marshal(items)
122182
if err != nil {
123183
return nil, fmt.Errorf("failed to marshal update items: %v", err)
@@ -138,6 +198,165 @@ func UpdateDependabotConfig(dependabotConfig string) (*UpdateDependabotConfigRes
138198
return response, nil
139199
}
140200

201+
// updateSubtractiveFields finds each ecosystem entry in the existing YAML config by
202+
// PackageEcosystem + Directory, then updates only the non-empty fields from the request,
203+
func updateSubtractiveFields(content string, ecosystems []Ecosystem, indent int) (string, bool, error) {
204+
var cfg Config
205+
if err := yaml.Unmarshal([]byte(content), &cfg); err != nil {
206+
return "", false, fmt.Errorf("failed to parse yaml: %w", err)
207+
}
208+
209+
existingChanged := false
210+
var toAdd []Ecosystem
211+
212+
for _, eco := range ecosystems {
213+
found := false
214+
for i, update := range cfg.Updates {
215+
if update.PackageEcosystem != eco.PackageEcosystem {
216+
continue
217+
}
218+
if update.Directory != eco.Directory && update.Directory != eco.Directory+"/" {
219+
continue
220+
}
221+
found = true
222+
223+
// Found the matching entry — update only non-empty fields.
224+
if eco.Interval != "" && cfg.Updates[i].Schedule.Interval != eco.Interval {
225+
cfg.Updates[i].Schedule.Interval = eco.Interval
226+
existingChanged = true
227+
}
228+
229+
if eco.CoolDown != nil {
230+
if cfg.Updates[i].CoolDown == nil {
231+
cfg.Updates[i].CoolDown = &CoolDown{}
232+
}
233+
existing := cfg.Updates[i].CoolDown
234+
cd := eco.CoolDown
235+
if cd.DefaultDays != 0 && existing.DefaultDays != cd.DefaultDays {
236+
existing.DefaultDays = cd.DefaultDays
237+
existingChanged = true
238+
}
239+
if cd.SemverMajorDays != 0 && existing.SemverMajorDays != cd.SemverMajorDays {
240+
existing.SemverMajorDays = cd.SemverMajorDays
241+
existingChanged = true
242+
}
243+
if cd.SemverMinorDays != 0 && existing.SemverMinorDays != cd.SemverMinorDays {
244+
existing.SemverMinorDays = cd.SemverMinorDays
245+
existingChanged = true
246+
}
247+
if cd.SemverPatchDays != 0 && existing.SemverPatchDays != cd.SemverPatchDays {
248+
existing.SemverPatchDays = cd.SemverPatchDays
249+
existingChanged = true
250+
}
251+
if len(cd.Include) > 0 {
252+
existing.Include = cd.Include
253+
existingChanged = true
254+
}
255+
if len(cd.Exclude) > 0 {
256+
existing.Exclude = cd.Exclude
257+
existingChanged = true
258+
}
259+
}
260+
261+
if len(eco.Groups) > 0 {
262+
if cfg.Updates[i].Groups == nil {
263+
cfg.Updates[i].Groups = make(map[string]Group)
264+
}
265+
for groupName, ecoGroup := range eco.Groups {
266+
existing, exists := cfg.Updates[i].Groups[groupName]
267+
if !exists {
268+
// New group — add it wholesale.
269+
cfg.Updates[i].Groups[groupName] = ecoGroup
270+
existingChanged = true
271+
continue
272+
}
273+
// Existing group — update only the non-empty fields.
274+
if ecoGroup.AppliesTo != "" && existing.AppliesTo != ecoGroup.AppliesTo {
275+
existing.AppliesTo = ecoGroup.AppliesTo
276+
existingChanged = true
277+
}
278+
if len(ecoGroup.Patterns) > 0 {
279+
existing.Patterns = ecoGroup.Patterns
280+
existingChanged = true
281+
}
282+
if len(ecoGroup.ExcludePatterns) > 0 {
283+
existing.ExcludePatterns = ecoGroup.ExcludePatterns
284+
existingChanged = true
285+
}
286+
if ecoGroup.DependencyType != "" && existing.DependencyType != ecoGroup.DependencyType {
287+
existing.DependencyType = ecoGroup.DependencyType
288+
existingChanged = true
289+
}
290+
if len(ecoGroup.UpdateTypes) > 0 {
291+
existing.UpdateTypes = ecoGroup.UpdateTypes
292+
existingChanged = true
293+
}
294+
if ecoGroup.GroupBy != "" && existing.GroupBy != ecoGroup.GroupBy {
295+
existing.GroupBy = ecoGroup.GroupBy
296+
existingChanged = true
297+
}
298+
cfg.Updates[i].Groups[groupName] = existing
299+
}
300+
}
301+
break
302+
}
303+
304+
if !found {
305+
toAdd = append(toAdd, eco)
306+
}
307+
}
308+
309+
if !existingChanged && len(toAdd) == 0 {
310+
return content, false, nil
311+
}
312+
313+
// Re-serialize existing entries only when they were actually modified;
314+
// otherwise preserve the original content so formatting is not disturbed.
315+
var finalOutput strings.Builder
316+
if existingChanged {
317+
var buf bytes.Buffer
318+
enc := yaml.NewEncoder(&buf)
319+
enc.SetIndent(indent)
320+
if err := enc.Encode(&cfg); err != nil {
321+
return "", false, fmt.Errorf("failed to marshal yaml: %w", err)
322+
}
323+
finalOutput.WriteString(buf.String())
324+
} else {
325+
finalOutput.WriteString(content)
326+
}
327+
328+
// Append new ecosystems using the same additive approach: marshal each entry
329+
// individually and WriteString with addIndentation so a blank line is
330+
// inserted before every new entry.
331+
for _, eco := range toAdd {
332+
item := ExtendedUpdate{
333+
Update: dependabot.Update{
334+
PackageEcosystem: eco.PackageEcosystem,
335+
Directory: eco.Directory,
336+
Schedule: dependabot.Schedule{Interval: eco.Interval},
337+
},
338+
Groups: eco.Groups,
339+
CoolDown: eco.CoolDown,
340+
}
341+
items := []ExtendedUpdate{item}
342+
var itemBuf bytes.Buffer
343+
itemEnc := yaml.NewEncoder(&itemBuf)
344+
itemEnc.SetIndent(indent)
345+
if err := itemEnc.Encode(items); err != nil {
346+
return "", false, fmt.Errorf("failed to marshal update items: %w", err)
347+
}
348+
// addIndentation expects a 1-indexed column; indent is already the space
349+
// count, so pass indent+1.
350+
data, err := addIndentation(itemBuf.String(), indent+1)
351+
if err != nil {
352+
return "", false, fmt.Errorf("failed to add indentation: %w", err)
353+
}
354+
finalOutput.WriteString(data)
355+
}
356+
357+
return finalOutput.String(), true, nil
358+
}
359+
141360
// addIndentation adds a certain number of spaces to the start of each line in the input string.
142361
// It returns a new string with the added indentation.
143362
func addIndentation(data string, indentation int) (string, error) {

0 commit comments

Comments
 (0)