cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.75.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/discovery/discovery.go

280lines · modecode

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