cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.77.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/discovery/discovery.go

281lines · modecode

1package discovery
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "log/slog"
9 "regexp"
10 "slices"
11 "time"
12
13 "github.com/cloudflare/pint/internal/comments"
14 "github.com/cloudflare/pint/internal/diags"
15 "github.com/cloudflare/pint/internal/parser"
16)
17
18const (
19 FileOwnerComment = "file/owner"
20 FileDisabledCheckComment = "file/disable"
21 FileSnoozeCheckComment = "file/snooze"
22 RuleOwnerComment = "rule/owner"
23)
24
25type FileIgnoreError struct {
26 Diagnostic diags.Diagnostic
27}
28
29func (fe FileIgnoreError) Error() string {
30 return fe.Diagnostic.Message
31}
32
33type ChangeType uint8
34
35func (c ChangeType) String() string {
36 switch c {
37 case Unknown:
38 return "unknown"
39 case Noop:
40 return "noop"
41 case Added:
42 return "added"
43 case Modified:
44 return "modified"
45 case Removed:
46 return "removed"
47 case Moved:
48 return "moved"
49 default:
50 return "---"
51 }
52}
53
54func (c *ChangeType) MarshalJSON() ([]byte, error) {
55 return json.Marshal(c.String())
56}
57
58const (
59 Unknown ChangeType = iota
60 Noop
61 Added
62 Modified
63 Removed
64 Moved
65)
66
67type Path struct {
68 Name string // File path, it can be symlink.
69 SymlinkTarget string // Symlink target, or the same as name if not a symlink.
70}
71
72func (p Path) String() string {
73 if p.Name == p.SymlinkTarget {
74 return p.Name
75 }
76 return fmt.Sprintf("%s ~> %s", p.Name, p.SymlinkTarget)
77}
78
79type Entry struct {
80 PathError error
81 File *parser.File `json:"-"`
82 Group *parser.Group `json:"-"`
83 Path Path
84 Owner string
85 ModifiedLines []int
86 DisabledChecks []string
87 Rule parser.Rule
88 State ChangeType
89}
90
91func (e Entry) Labels() (ym parser.YamlMap) {
92 switch {
93 case e.Rule.AlertingRule != nil && e.Rule.AlertingRule.Labels != nil:
94 ym.Key = e.Rule.AlertingRule.Labels.Key
95 if e.Group != nil && e.Group.Labels != nil {
96 ym.Items = parser.MergeMaps(e.Group.Labels, e.Rule.AlertingRule.Labels).Items
97 } else {
98 ym.Items = e.Rule.AlertingRule.Labels.Items
99 }
100 case e.Rule.RecordingRule != nil && e.Rule.RecordingRule.Labels != nil:
101 ym.Key = e.Rule.RecordingRule.Labels.Key
102 if e.Group != nil && e.Group.Labels != nil {
103 ym.Items = parser.MergeMaps(e.Group.Labels, e.Rule.RecordingRule.Labels).Items
104 } else {
105 ym.Items = e.Rule.RecordingRule.Labels.Items
106 }
107 case e.Group != nil && e.Group.Labels != nil:
108 ym.Key = e.Group.Labels.Key
109 ym.Items = e.Group.Labels.Items
110 }
111 return ym
112}
113
114func readRules(reportedPath, sourcePath string, r io.Reader, p parser.Parser, allowedOwners []*regexp.Regexp) (entries []Entry, _ error) {
115 file := p.Parse(r)
116
117 contentLines := diags.LineRange{
118 First: min(file.TotalLines, 1),
119 Last: file.TotalLines,
120 }
121
122 var badOwners []comments.Comment
123 var fileOwner string
124 var disabledChecks []string
125 for _, comment := range file.Comments {
126 // nolint:exhaustive
127 switch comment.Type {
128 case comments.FileOwnerType:
129 owner := comment.Value.(comments.Owner)
130 if isValidOwner(owner.Name, allowedOwners) {
131 fileOwner = owner.Name
132 } else {
133 badOwners = append(badOwners, comment)
134 }
135 case comments.FileDisableType:
136 disable := comment.Value.(comments.Disable)
137 if !slices.Contains(disabledChecks, disable.Match) {
138 disabledChecks = append(disabledChecks, disable.Match)
139 }
140 case comments.FileSnoozeType:
141 snooze := comment.Value.(comments.Snooze)
142 if !snooze.Until.After(time.Now()) {
143 continue
144 }
145 if !slices.Contains(disabledChecks, snooze.Match) {
146 disabledChecks = append(disabledChecks, snooze.Match)
147 }
148 slog.LogAttrs(context.Background(), slog.LevelDebug,
149 "Check snoozed by comment",
150 slog.String("check", snooze.Match),
151 slog.String("match", snooze.Match),
152 slog.Time("until", snooze.Until),
153 )
154 case comments.InvalidComment:
155 entries = append(entries, Entry{
156 Path: Path{
157 Name: sourcePath,
158 SymlinkTarget: reportedPath,
159 },
160 PathError: comment.Value.(comments.Invalid).Err,
161 Owner: fileOwner,
162 ModifiedLines: contentLines.Expand(),
163 })
164 }
165 }
166
167 for _, d := range file.Diagnostics {
168 entries = append(entries, Entry{
169 Path: Path{
170 Name: sourcePath,
171 SymlinkTarget: reportedPath,
172 },
173 PathError: FileIgnoreError{
174 Diagnostic: d,
175 },
176 Owner: fileOwner,
177 ModifiedLines: contentLines.Expand(),
178 })
179 return entries, nil
180 }
181
182 if file.Error.Err != nil {
183 slog.LogAttrs(context.Background(), slog.LevelWarn,
184 "Failed to parse file content",
185 slog.Any("err", file.Error.Err),
186 slog.String("path", sourcePath),
187 slog.Int("line", file.Error.Line),
188 )
189 entries = append(entries, Entry{
190 Path: Path{
191 Name: sourcePath,
192 SymlinkTarget: reportedPath,
193 },
194 PathError: file.Error,
195 Owner: fileOwner,
196 ModifiedLines: contentLines.Expand(),
197 })
198 return entries, nil
199 }
200
201 for _, group := range file.Groups {
202 if group.Error.Err != nil {
203 slog.LogAttrs(context.Background(), slog.LevelWarn,
204 "Failed to parse group entry",
205 slog.Any("err", group.Error.Err),
206 slog.String("path", sourcePath),
207 slog.Int("line", group.Error.Line),
208 )
209 entries = append(entries, Entry{
210 Path: Path{
211 Name: sourcePath,
212 SymlinkTarget: reportedPath,
213 },
214 PathError: group.Error,
215 Owner: fileOwner,
216 ModifiedLines: []int{group.Error.Line},
217 })
218 }
219 for _, rule := range group.Rules {
220 ruleOwner := fileOwner
221 for _, owner := range comments.Only[comments.Owner](rule.Comments, comments.RuleOwnerType) {
222 ruleOwner = owner.Name
223 }
224 entries = append(entries, Entry{
225 Path: Path{
226 Name: sourcePath,
227 SymlinkTarget: reportedPath,
228 },
229 File: &file,
230 Group: &group,
231 Rule: rule,
232 ModifiedLines: rule.Lines.Expand(),
233 Owner: ruleOwner,
234 DisabledChecks: disabledChecks,
235 })
236 }
237 }
238
239 if len(entries) == 0 && len(badOwners) > 0 {
240 for _, comment := range badOwners {
241 owner := comment.Value.(comments.Owner)
242 entries = append(entries, Entry{
243 Path: Path{
244 Name: sourcePath,
245 SymlinkTarget: reportedPath,
246 },
247 PathError: comments.OwnerError{
248 Diagnostic: diags.Diagnostic{
249 Message: fmt.Sprintf("This file is set as owned by `%s` but `%s` doesn't match any of the allowed owner values.", owner.Name, owner.Name),
250 Pos: diags.PositionRanges{
251 {
252 Line: owner.Line,
253 FirstColumn: comment.Offset + 1,
254 LastColumn: comment.Offset + len(owner.Name),
255 },
256 },
257 FirstColumn: comment.Offset + 1,
258 LastColumn: comment.Offset + len(owner.Name),
259 Kind: diags.Issue,
260 },
261 },
262 ModifiedLines: contentLines.Expand(),
263 })
264 }
265 }
266
267 slog.LogAttrs(context.Background(), slog.LevelDebug, "File parsed", slog.String("path", sourcePath), slog.Int("rules", len(entries)))
268 return entries, nil
269}
270
271func isValidOwner(s string, valid []*regexp.Regexp) bool {
272 if len(valid) == 0 {
273 return true
274 }
275 for _, v := range valid {
276 if v.MatchString(s) {
277 return true
278 }
279 }
280 return false
281}