• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mindersec / minder / 12360611398

16 Dec 2024 08:20PM UTC coverage: 55.476% (+0.1%) from 55.374%
12360611398

Pull #5181

github

web-flow
Merge c6abe06ac into 5e3b3c802
Pull Request #5181: Add support for base and target trees in git ingest, add .tar.gz bundler

302 of 416 new or added lines in 10 files covered. (72.6%)

9 existing lines in 3 files now uncovered.

16963 of 30577 relevant lines covered (55.48%)

38.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

46.21
/internal/controlplane/handlers_ruletype.go
1
// SPDX-FileCopyrightText: Copyright 2023 The Minder Authors
2
// SPDX-License-Identifier: Apache-2.0
3

4
package controlplane
5

6
import (
7
        "bytes"
8
        "context"
9
        "database/sql"
10
        "errors"
11
        "fmt"
12
        "strings"
13
        "unicode/utf8"
14

15
        "github.com/google/uuid"
16
        "github.com/microcosm-cc/bluemonday"
17
        "github.com/open-feature/go-sdk/openfeature"
18
        "github.com/yuin/goldmark"
19
        "github.com/yuin/goldmark/extension"
20
        "github.com/yuin/goldmark/parser"
21
        "github.com/yuin/goldmark/renderer/html"
22
        "google.golang.org/grpc/codes"
23
        "google.golang.org/grpc/status"
24

25
        "github.com/mindersec/minder/internal/db"
26
        "github.com/mindersec/minder/internal/engine/engcontext"
27
        "github.com/mindersec/minder/internal/engine/ingester/git"
28
        "github.com/mindersec/minder/internal/flags"
29
        "github.com/mindersec/minder/internal/logger"
30
        "github.com/mindersec/minder/internal/util"
31
        minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
32
        "github.com/mindersec/minder/pkg/ruletypes"
33
)
34

35
var (
36
        maxReadableStringSize = 3 * 1 << 10 // 3kB
37
)
38

39
var (
40
        errInvalidRuleType = errors.New("invalid rule type")
41
)
42

43
// ListRuleTypes is a method to list all rule types for a given context
44
func (s *Server) ListRuleTypes(
45
        ctx context.Context,
46
        _ *minderv1.ListRuleTypesRequest,
47
) (*minderv1.ListRuleTypesResponse, error) {
4✔
48
        entityCtx := engcontext.EntityFromContext(ctx)
4✔
49

4✔
50
        err := entityCtx.ValidateProject(ctx, s.store)
4✔
51
        if err != nil {
5✔
52
                return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
1✔
53
        }
1✔
54

55
        lrt, err := s.store.ListRuleTypesByProject(ctx, entityCtx.Project.ID)
3✔
56
        if err != nil {
4✔
57
                return nil, status.Errorf(codes.Unknown, "failed to get rule types: %s", err)
1✔
58
        }
1✔
59

60
        resp := &minderv1.ListRuleTypesResponse{}
2✔
61

2✔
62
        for idx := range lrt {
4✔
63
                rt := lrt[idx]
2✔
64
                rtpb, err := ruletypes.RuleTypePBFromDB(&rt)
2✔
65
                if err != nil {
2✔
66
                        return nil, fmt.Errorf("cannot convert rule type %s to pb: %v", rt.Name, err)
×
67
                }
×
68

69
                resp.RuleTypes = append(resp.RuleTypes, rtpb)
2✔
70
        }
71

72
        // Telemetry logging
73
        logger.BusinessRecord(ctx).Project = entityCtx.Project.ID
2✔
74

2✔
75
        return resp, nil
2✔
76
}
77

78
// GetRuleTypeByName is a method to get a rule type by name
79
func (s *Server) GetRuleTypeByName(
80
        ctx context.Context,
81
        in *minderv1.GetRuleTypeByNameRequest,
82
) (*minderv1.GetRuleTypeByNameResponse, error) {
×
83
        entityCtx := engcontext.EntityFromContext(ctx)
×
84

×
85
        err := entityCtx.ValidateProject(ctx, s.store)
×
86
        if err != nil {
×
87
                return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
×
88
        }
×
89

90
        resp := &minderv1.GetRuleTypeByNameResponse{}
×
91

×
92
        rtdb, err := s.store.GetRuleTypeByName(ctx, db.GetRuleTypeByNameParams{
×
93
                // TODO: Add option to fetch rule types from parent projects too
×
94
                Projects: []uuid.UUID{entityCtx.Project.ID},
×
95
                Name:     in.GetName(),
×
96
        })
×
97
        if errors.Is(err, sql.ErrNoRows) {
×
98
                return nil, util.UserVisibleError(codes.NotFound, "rule type %s not found", in.GetName())
×
99
        } else if err != nil {
×
100
                return nil, status.Errorf(codes.Unknown, "failed to get rule type: %s", err)
×
101
        }
×
102

103
        rt, err := ruletypes.RuleTypePBFromDB(&rtdb)
×
104
        if err != nil {
×
105
                return nil, fmt.Errorf("cannot convert rule type %s to pb: %v", rtdb.Name, err)
×
106
        }
×
107

108
        resp.RuleType = rt
×
109

×
110
        // Telemetry logging
×
111
        logger.BusinessRecord(ctx).Project = rtdb.ProjectID
×
112
        logger.BusinessRecord(ctx).RuleType = logger.RuleType{Name: rtdb.Name, ID: rtdb.ID}
×
113

×
114
        return resp, nil
×
115
}
116

117
// GetRuleTypeById is a method to get a rule type by id
118
func (s *Server) GetRuleTypeById(
119
        ctx context.Context,
120
        in *minderv1.GetRuleTypeByIdRequest,
121
) (*minderv1.GetRuleTypeByIdResponse, error) {
×
122
        entityCtx := engcontext.EntityFromContext(ctx)
×
123

×
124
        err := entityCtx.ValidateProject(ctx, s.store)
×
125
        if err != nil {
×
126
                return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
×
127
        }
×
128

129
        resp := &minderv1.GetRuleTypeByIdResponse{}
×
130

×
131
        parsedRuleTypeID, err := uuid.Parse(in.GetId())
×
132
        if err != nil {
×
133
                return nil, util.UserVisibleError(codes.InvalidArgument, "invalid rule type ID")
×
134
        }
×
135

136
        rtdb, err := s.store.GetRuleTypeByID(ctx, parsedRuleTypeID)
×
137
        if errors.Is(err, sql.ErrNoRows) {
×
138
                return nil, util.UserVisibleError(codes.NotFound, "rule type %s not found", in.GetId())
×
139
        } else if err != nil {
×
140
                return nil, status.Errorf(codes.Unknown, "failed to get rule type: %s", err)
×
141
        }
×
142

143
        rt, err := ruletypes.RuleTypePBFromDB(&rtdb)
×
144
        if err != nil {
×
145
                return nil, fmt.Errorf("cannot convert rule type %s to pb: %v", rtdb.Name, err)
×
146
        }
×
147

148
        resp.RuleType = rt
×
149

×
150
        // Telemetry logging
×
151
        logger.BusinessRecord(ctx).Project = rtdb.ProjectID
×
152
        logger.BusinessRecord(ctx).RuleType = logger.RuleType{Name: rtdb.Name, ID: rtdb.ID}
×
153

×
154
        return resp, nil
×
155
}
156

157
// CreateRuleType is a method to create a rule type
158
func (s *Server) CreateRuleType(
159
        ctx context.Context,
160
        crt *minderv1.CreateRuleTypeRequest,
161
) (*minderv1.CreateRuleTypeResponse, error) {
4✔
162
        entityCtx := engcontext.EntityFromContext(ctx)
4✔
163
        err := entityCtx.ValidateProject(ctx, s.store)
4✔
164
        if err != nil {
4✔
165
                return nil, util.UserVisibleError(codes.InvalidArgument, "error in entity context: %v", err)
×
166
        }
×
167

168
        projectID := entityCtx.Project.ID
4✔
169

4✔
170
        if err := validateSizeAndUTF8(crt.RuleType.Guidance); err != nil {
6✔
171
                return nil, util.UserVisibleError(codes.InvalidArgument, "%s", err)
2✔
172
        }
2✔
173
        if err := sanitizeMarkdown(crt.RuleType.Guidance); err != nil {
3✔
174
                return nil, util.UserVisibleError(codes.InvalidArgument, "%s", err)
1✔
175
        }
1✔
176
        if err := validateMarkdown(crt.RuleType.Guidance); err != nil {
1✔
177
                return nil, util.UserVisibleError(codes.InvalidArgument, "%s", err)
×
178
        }
×
179

180
        ruleDef := crt.GetRuleType().GetDef()
1✔
181
        if err := checkRuleDefinitionFlags(ctx, s.featureFlags, ruleDef); err != nil {
1✔
NEW
182
                return nil, err
×
UNCOV
183
        }
×
184

185
        newRuleType, err := db.WithTransaction(s.store, func(qtx db.ExtendQuerier) (*minderv1.RuleType, error) {
2✔
186
                return s.ruleTypes.CreateRuleType(ctx, projectID, uuid.Nil, crt.GetRuleType(), qtx)
1✔
187
        })
1✔
188
        if err != nil {
1✔
189
                if errors.Is(err, ruletypes.ErrRuleTypeInvalid) {
×
190
                        return nil, util.UserVisibleError(codes.InvalidArgument, "invalid rule type definition: %s", err)
×
191
                } else if errors.Is(err, ruletypes.ErrRuleAlreadyExists) {
×
192
                        return nil, util.UserVisibleError(codes.AlreadyExists, "rule type %s already exists", crt.RuleType.GetName())
×
193
                } else if errors.Is(err, ruletypes.ErrDataSourceNotFound) {
×
194
                        return nil, util.UserVisibleError(codes.InvalidArgument, "%s", err.Error())
×
195
                }
×
196
                return nil, status.Errorf(codes.Unknown, "failed to create rule type: %s", err)
×
197
        }
198

199
        return &minderv1.CreateRuleTypeResponse{
1✔
200
                RuleType: newRuleType,
1✔
201
        }, nil
1✔
202
}
203

204
func checkRuleDefinitionFlags(
205
        ctx context.Context, featureFlags openfeature.IClient, ruleDef *minderv1.RuleType_Definition) *util.NiceStatus {
2✔
206
        ruleDS := ruleDef.GetEval().GetDataSources()
2✔
207
        if len(ruleDS) > 0 && !flags.Bool(ctx, featureFlags, flags.DataSources) {
2✔
NEW
208
                return util.UserVisibleError(codes.InvalidArgument, "DataSources feature is disabled")
×
NEW
209
        }
×
210

211
        prCommentAlert := ruleDef.GetAlert().GetPullRequestComment()
2✔
212
        if prCommentAlert != nil && !flags.Bool(ctx, featureFlags, flags.PRCommentAlert) {
2✔
NEW
213
                return util.UserVisibleError(codes.InvalidArgument, "Pull request comment alert type is disabled")
×
NEW
214
        }
×
215

216
        usesGitPR := ruleDef.GetIngest().GetType() == git.GitRuleDataIngestType &&
2✔
217
                ruleDef.GetInEntity() == minderv1.PullRequestEntity.String()
2✔
218
        if usesGitPR && !flags.Bool(ctx, featureFlags, flags.GitPRDiffs) {
2✔
NEW
219
                return util.UserVisibleError(codes.InvalidArgument, "Git pull request ingest is disabled")
×
NEW
220
        }
×
221

222
        return nil
2✔
223
}
224

225
// UpdateRuleType is a method to update a rule type
226
func (s *Server) UpdateRuleType(
227
        ctx context.Context,
228
        urt *minderv1.UpdateRuleTypeRequest,
229
) (*minderv1.UpdateRuleTypeResponse, error) {
4✔
230
        entityCtx := engcontext.EntityFromContext(ctx)
4✔
231
        err := entityCtx.ValidateProject(ctx, s.store)
4✔
232
        if err != nil {
4✔
233
                return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
×
234
        }
×
235

236
        projectID := entityCtx.Project.ID
4✔
237

4✔
238
        if err := validateSizeAndUTF8(urt.RuleType.Guidance); err != nil {
6✔
239
                return nil, util.UserVisibleError(codes.InvalidArgument, "%s", err)
2✔
240
        }
2✔
241
        if err := sanitizeMarkdown(urt.RuleType.Guidance); err != nil {
3✔
242
                return nil, util.UserVisibleError(codes.InvalidArgument, "%s", err)
1✔
243
        }
1✔
244
        if err := validateMarkdown(urt.RuleType.Guidance); err != nil {
1✔
245
                return nil, util.UserVisibleError(codes.InvalidArgument, "%s", err)
×
246
        }
×
247

248
        ruleDef := urt.GetRuleType().GetDef()
1✔
249
        if err := checkRuleDefinitionFlags(ctx, s.featureFlags, ruleDef); err != nil {
1✔
NEW
250
                return nil, err
×
UNCOV
251
        }
×
252

253
        updatedRuleType, err := db.WithTransaction(s.store, func(qtx db.ExtendQuerier) (*minderv1.RuleType, error) {
2✔
254
                return s.ruleTypes.UpdateRuleType(ctx, projectID, uuid.Nil, urt.GetRuleType(), qtx)
1✔
255
        })
1✔
256
        if err != nil {
1✔
257
                if errors.Is(err, ruletypes.ErrRuleTypeInvalid) {
×
258
                        return nil, util.UserVisibleError(codes.InvalidArgument, "invalid rule type definition: %s", err)
×
259
                } else if errors.Is(err, ruletypes.ErrRuleNotFound) {
×
260
                        return nil, status.Errorf(codes.NotFound, "rule type %s not found", urt.RuleType.GetName())
×
261
                }
×
262
                return nil, status.Errorf(codes.Unknown, "failed to update rule type: %s", err)
×
263
        }
264

265
        return &minderv1.UpdateRuleTypeResponse{
1✔
266
                RuleType: updatedRuleType,
1✔
267
        }, nil
1✔
268
}
269

270
// DeleteRuleType is a method to delete a rule type
271
func (s *Server) DeleteRuleType(
272
        ctx context.Context,
273
        in *minderv1.DeleteRuleTypeRequest,
274
) (*minderv1.DeleteRuleTypeResponse, error) {
×
275
        parsedRuleTypeID, err := uuid.Parse(in.GetId())
×
276
        if err != nil {
×
277
                return nil, util.UserVisibleError(codes.InvalidArgument, "invalid rule type ID")
×
278
        }
×
279

280
        // first read rule type by id, so we can get provider
281
        rtdb, err := s.store.GetRuleTypeByID(ctx, parsedRuleTypeID)
×
282
        if err != nil {
×
283
                if errors.Is(err, sql.ErrNoRows) {
×
284
                        return nil, util.UserVisibleError(codes.NotFound, "rule type %s not found", in.GetId())
×
285
                }
×
286
                return nil, status.Errorf(codes.Unknown, "failed to get rule type: %s", err)
×
287
        }
288

289
        // TEMPORARY HACK: Since we do not need to support the deletion of bundle
290
        // rule types yet, reject them in the API
291
        // TODO: Move this deletion logic to RuleTypeService
292
        if rtdb.SubscriptionID.Valid {
×
293
                return nil, status.Errorf(codes.InvalidArgument, "cannot delete rule type from bundle")
×
294
        }
×
295

296
        entityCtx := engcontext.EntityFromContext(ctx)
×
297

×
298
        err = entityCtx.ValidateProject(ctx, s.store)
×
299
        if err != nil {
×
300
                return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
×
301
        }
×
302

303
        profiles, err := s.store.ListProfilesInstantiatingRuleType(ctx, rtdb.ID)
×
304
        // We have profiles that use this rule type, so we can't delete it
×
305
        if err == nil {
×
306
                if len(profiles) > 0 {
×
307
                        return nil, util.UserVisibleError(codes.FailedPrecondition,
×
308
                                "cannot delete: rule type %s is used by profiles %s", in.GetId(), strings.Join(profiles, ", "))
×
309
                }
×
310
        } else if !errors.Is(err, sql.ErrNoRows) {
×
311
                // If we failed for another reason, return an error
×
312
                return nil, status.Errorf(codes.Unknown, "failed to get profiles: %s", err)
×
313
        }
×
314

315
        // If there are no profiles instantiating this rule type, we can delete it
316
        err = s.store.DeleteRuleType(ctx, parsedRuleTypeID)
×
317
        if err != nil {
×
318
                // The rule got deleted in parallel?
×
319
                if errors.Is(err, sql.ErrNoRows) {
×
320
                        return nil, util.UserVisibleError(codes.NotFound, "rule type %s not found", in.GetId())
×
321
                }
×
322
                return nil, status.Errorf(codes.Unknown, "failed to delete rule type: %s", err)
×
323
        }
324

325
        // Telemetry logging
326
        logger.BusinessRecord(ctx).Project = rtdb.ProjectID
×
327
        logger.BusinessRecord(ctx).RuleType = logger.RuleType{Name: rtdb.Name, ID: rtdb.ID}
×
328

×
329
        return &minderv1.DeleteRuleTypeResponse{}, nil
×
330
}
331

332
var (
333
        allowedPlainChars = []string{"'", "\""}
334
        allowedEncodings  = []string{"&#39;", "&#34;"}
335
)
336

337
func validateSizeAndUTF8(s string) error {
8✔
338
        // As of the time of this writing, Minder profiles and rules
8✔
339
        // have a guidance that's less the maximum allowed size for
8✔
340
        // human-readable strings.
8✔
341
        if len(s) > maxReadableStringSize {
10✔
342
                return errors.New("too long")
2✔
343
        }
2✔
344

345
        if !utf8.ValidString(s) {
8✔
346
                return errors.New("not valid utf-8")
2✔
347
        }
2✔
348

349
        return nil
4✔
350
}
351

352
func sanitizeMarkdown(md string) error {
4✔
353
        p := bluemonday.StrictPolicy()
4✔
354

4✔
355
        // The following two for loops remove characters that we want
4✔
356
        // to allow from both the source string and the sanitized
4✔
357
        // version, so that we can compare the two to verify that no
4✔
358
        // other HTML content is there.
4✔
359
        sanitized := p.Sanitize(md)
4✔
360
        for _, c := range allowedEncodings {
12✔
361
                sanitized = strings.ReplaceAll(sanitized, c, "")
8✔
362
        }
8✔
363
        for _, c := range allowedPlainChars {
12✔
364
                md = strings.ReplaceAll(md, c, "")
8✔
365
        }
8✔
366

367
        if sanitized != md {
6✔
368
                return fmt.Errorf("%w: value contains html", errInvalidRuleType)
2✔
369
        }
2✔
370

371
        return nil
2✔
372
}
373

374
func validateMarkdown(md string) error {
2✔
375
        // The following lines validate that `md` is valid, parseable
2✔
376
        // markdown. Be mindful that any UTF-8 string is valid
2✔
377
        // markdown, so this is redundant at the moment. Should we
2✔
378
        // accept byte slices in place of strings, this check would
2✔
379
        // become much more relevant.
2✔
380
        gm := goldmark.New(
2✔
381
                // GitHub Flavored Markdown
2✔
382
                goldmark.WithExtensions(extension.GFM),
2✔
383
                goldmark.WithParserOptions(
2✔
384
                        parser.WithAutoHeadingID(),
2✔
385
                ),
2✔
386
                goldmark.WithRendererOptions(
2✔
387
                        html.WithHardWraps(),
2✔
388
                        html.WithXHTML(),
2✔
389
                ),
2✔
390
        )
2✔
391
        if err := gm.Convert([]byte(md), &bytes.Buffer{}); err != nil {
2✔
392
                return fmt.Errorf(
×
393
                        "%w: %s",
×
394
                        errInvalidRuleType,
×
395
                        err,
×
396
                )
×
397
        }
×
398

399
        return nil
2✔
400
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc