cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.83.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/discovery.go

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