• 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

80.62
/internal/engine/eval/rego/lib.go
1
// SPDX-FileCopyrightText: Copyright 2023 The Minder Authors
2
// SPDX-License-Identifier: Apache-2.0
3

4
package rego
5

6
import (
7
        "archive/tar"
8
        "bytes"
9
        "compress/gzip"
10
        "context"
11
        "errors"
12
        "fmt"
13
        "io"
14
        "io/fs"
15
        "net/http"
16
        "os"
17
        "path/filepath"
18
        "time"
19

20
        "github.com/go-git/go-billy/v5"
21
        billyutil "github.com/go-git/go-billy/v5/util"
22
        "github.com/open-feature/go-sdk/openfeature"
23
        "github.com/open-policy-agent/opa/ast"
24
        "github.com/open-policy-agent/opa/rego"
25
        "github.com/open-policy-agent/opa/types"
26
        "github.com/stacklok/frizbee/pkg/replacer"
27
        "github.com/stacklok/frizbee/pkg/utils/config"
28
        "gopkg.in/yaml.v3"
29

30
        "github.com/mindersec/minder/internal/flags"
31
        "github.com/mindersec/minder/internal/util"
32
        "github.com/mindersec/minder/pkg/engine/v1/interfaces"
33
)
34

35
// MinderRegoLib contains the minder-specific functions for rego
36
var MinderRegoLib = []func(res *interfaces.Result) func(*rego.Rego){
37
        FileExists,
38
        FileLs,
39
        FileLsGlob,
40
        FileHTTPType,
41
        FileRead,
42
        FileWalk,
43
        ListGithubActions,
44
        ParseYaml,
45
        JQIsTrue,
46
}
47

48
// MinderRegoLibExperiments contains Minder-specific functions which
49
// should only be exposed when the given experiment is enabled.
50
var MinderRegoLibExperiments = map[flags.Experiment][]func(res *interfaces.Result) func(*rego.Rego){
51
        flags.TarGzFunctions: {FileArchive, BaseFileArchive},
52
        flags.GitPRDiffs: {
53
                BaseFileExists,
54
                BaseFileLs,
55
                BaseFileLsGlob,
56
                BaseFileHTTPType,
57
                BaseFileRead,
58
                BaseFileWalk,
59
                BaseListGithubActions,
60
        },
61
}
62

63
func instantiateRegoLib(ctx context.Context, featureFlags openfeature.IClient, res *interfaces.Result) []func(*rego.Rego) {
56✔
64
        var lib []func(*rego.Rego)
56✔
65
        for _, f := range MinderRegoLib {
560✔
66
                lib = append(lib, f(res))
504✔
67
        }
504✔
68
        for flag, funcs := range MinderRegoLibExperiments {
168✔
69
                if flags.Bool(ctx, featureFlags, flag) {
114✔
70
                        for _, f := range funcs {
11✔
71
                                lib = append(lib, f(res))
9✔
72
                        }
9✔
73
                }
74
        }
75
        return lib
56✔
76
}
77

78
// FileExists is a rego function that checks if a file exists
79
// in the filesystem being evaluated (which comes from the ingester).
80
// It takes one argument, the path to the file to check.
81
// It's exposed as `file.exists`.
82
func FileExists(res *interfaces.Result) func(*rego.Rego) {
56✔
83
        return rego.Function1(
56✔
84
                &rego.Function{
56✔
85
                        Name: "file.exists",
56✔
86
                        Decl: types.NewFunction(types.Args(types.S), types.B),
56✔
87
                },
56✔
88
                fsExists(res.Fs),
56✔
89
        )
56✔
90
}
56✔
91

92
// BaseFileExists is a rego function that checks if a file exists
93
// in the base filesystem from the ingester.  Base filesystems are
94
// typically associated with pull requests.
95
// It takes one argument, the path to the file to check.
96
// It's exposed as `base_file.exists`.
97
func BaseFileExists(res *interfaces.Result) func(*rego.Rego) {
1✔
98
        return rego.Function1(
1✔
99
                &rego.Function{
1✔
100
                        Name: "base_file.exists",
1✔
101
                        Decl: types.NewFunction(types.Args(types.S), types.B),
1✔
102
                },
1✔
103
                fsExists(res.BaseFs),
1✔
104
        )
1✔
105
}
1✔
106

107
func fsExists(vfs billy.Filesystem) func(rego.BuiltinContext, *ast.Term) (*ast.Term, error) {
57✔
108
        return func(_ rego.BuiltinContext, op1 *ast.Term) (*ast.Term, error) {
61✔
109
                var path string
4✔
110
                if err := ast.As(op1.Value, &path); err != nil {
4✔
NEW
111
                        return nil, err
×
NEW
112
                }
×
113

114
                if vfs == nil {
4✔
NEW
115
                        return nil, fmt.Errorf("cannot check file existence without a filesystem")
×
NEW
116
                }
×
117

118
                cpath := filepath.Clean(path)
4✔
119
                _, err := vfs.Stat(cpath)
4✔
120
                if err != nil {
5✔
121
                        if errors.Is(err, os.ErrNotExist) {
2✔
122
                                return ast.BooleanTerm(false), nil
1✔
123
                        }
1✔
NEW
124
                        return nil, err
×
125
                }
126

127
                return ast.BooleanTerm(true), nil
3✔
128
        }
129
}
130

131
// FileRead is a rego function that reads a file from the filesystem
132
// being evaluated (which comes from the ingester). It takes one argument,
133
// the path to the file to read. It's exposed as `file.read`.
134
func FileRead(res *interfaces.Result) func(*rego.Rego) {
56✔
135
        return rego.Function1(
56✔
136
                &rego.Function{
56✔
137
                        Name: "file.read",
56✔
138
                        Decl: types.NewFunction(types.Args(types.S), types.S),
56✔
139
                },
56✔
140
                fsRead(res.Fs),
56✔
141
        )
56✔
142
}
56✔
143

144
// BaseFileRead is a rego function that reads a file from the
145
// base filesystem in a pull_request or other diff context.
146
// It takes one argument, the path to the file to read.
147
// It's exposed as `base_file.read`.
148
func BaseFileRead(res *interfaces.Result) func(*rego.Rego) {
1✔
149
        return rego.Function1(
1✔
150
                &rego.Function{
1✔
151
                        Name: "base_file.read",
1✔
152
                        Decl: types.NewFunction(types.Args(types.S), types.S),
1✔
153
                },
1✔
154
                fsRead(res.BaseFs),
1✔
155
        )
1✔
156
}
1✔
157

158
func fsRead(vfs billy.Filesystem) func(rego.BuiltinContext, *ast.Term) (*ast.Term, error) {
57✔
159
        return func(_ rego.BuiltinContext, op1 *ast.Term) (*ast.Term, error) {
69✔
160
                var path string
12✔
161
                if err := ast.As(op1.Value, &path); err != nil {
12✔
NEW
162
                        return nil, err
×
NEW
163
                }
×
164

165
                if vfs == nil {
12✔
NEW
166
                        return nil, fmt.Errorf("cannot read file without a filesystem")
×
NEW
167
                }
×
168

169
                cpath := filepath.Clean(path)
12✔
170
                f, err := vfs.Open(cpath)
12✔
171
                if err != nil {
12✔
NEW
172
                        return nil, err
×
NEW
173
                }
×
174

175
                defer f.Close()
12✔
176

12✔
177
                all, rerr := io.ReadAll(f)
12✔
178
                if rerr != nil {
12✔
NEW
179
                        return nil, rerr
×
NEW
180
                }
×
181

182
                allstr := ast.String(all)
12✔
183
                return ast.NewTerm(allstr), nil
12✔
184
        }
185
}
186

187
// FileLs is a rego function that lists the files in a directory
188
// in the filesystem being evaluated (which comes from the ingester).
189
// It takes one argument, the path to the directory to list. It's exposed
190
// as `file.ls`.
191
// If the file is a file, it returns the file itself.
192
// If the file is a directory, it returns the files in the directory.
193
// If the file is a symlink, it follows the symlink and returns the files
194
// in the target.
195
func FileLs(res *interfaces.Result) func(*rego.Rego) {
56✔
196
        return rego.Function1(
56✔
197
                &rego.Function{
56✔
198
                        Name: "file.ls",
56✔
199
                        Decl: types.NewFunction(types.Args(types.S), types.A),
56✔
200
                },
56✔
201
                fsLs(res.Fs),
56✔
202
        )
56✔
203
}
56✔
204

205
// BaseFileLs is a rego function that lists the files in a directory
206
// in the base filesystem being evaluated (in a pull_request or other
207
// diff context).  It takes one argument, the path to the directory to list.
208
// It's exposed as `base_file.ls`.
209
// If the file is a file, it returns the file itself.
210
// If the file is a directory, it returns the files in the directory.
211
// If the file is a symlink, it follows the symlink and returns the files
212
// in the target.
213
func BaseFileLs(res *interfaces.Result) func(*rego.Rego) {
1✔
214
        return rego.Function1(
1✔
215
                &rego.Function{
1✔
216
                        Name: "base_file.ls",
1✔
217
                        Decl: types.NewFunction(types.Args(types.S), types.A),
1✔
218
                },
1✔
219
                fsLs(res.BaseFs),
1✔
220
        )
1✔
221
}
1✔
222

223
func fsLs(vfs billy.Filesystem) func(rego.BuiltinContext, *ast.Term) (*ast.Term, error) {
57✔
224
        return func(_ rego.BuiltinContext, op1 *ast.Term) (*ast.Term, error) {
64✔
225
                var path string
7✔
226
                if err := ast.As(op1.Value, &path); err != nil {
7✔
NEW
227
                        return nil, err
×
NEW
228
                }
×
229

230
                if vfs == nil {
7✔
NEW
231
                        return nil, fmt.Errorf("cannot walk file without a filesystem")
×
NEW
232
                }
×
233

234
                // Check file information and return a list of files
235
                // and directories
236
                finfo, err := vfs.Lstat(path)
7✔
237
                if err != nil {
8✔
238
                        return fileLsHandleError(err)
1✔
239
                }
1✔
240

241
                // If the file is a file return the file itself
242
                if finfo.Mode().IsRegular() {
7✔
243
                        return fileLsHandleFile(path)
1✔
244
                }
1✔
245

246
                // If the file is a directory return the files in the directory
247
                if finfo.Mode().IsDir() {
8✔
248
                        return fileLsHandleDir(path, vfs)
3✔
249
                }
3✔
250

251
                // If the file is a symlink, follow it
252
                if finfo.Mode()&os.ModeSymlink != 0 {
4✔
253
                        // Get the target of the symlink
2✔
254
                        target, err := vfs.Readlink(path)
2✔
255
                        if err != nil {
2✔
NEW
256
                                return nil, err
×
NEW
257
                        }
×
258

259
                        // Get the file information of the target
260
                        // NOTE: This overwrites the previous finfo
261
                        finfo, err = vfs.Lstat(target)
2✔
262
                        if err != nil {
2✔
UNCOV
263
                                return fileLsHandleError(err)
×
UNCOV
264
                        }
×
265

266
                        // If the target is a file return the file itself
267
                        if finfo.Mode().IsRegular() {
3✔
268
                                return fileLsHandleFile(target)
1✔
269
                        }
1✔
270

271
                        // If the target is a directory return the files in the directory
272
                        if finfo.Mode().IsDir() {
2✔
273
                                return fileLsHandleDir(target, vfs)
1✔
274
                        }
1✔
275
                }
276

NEW
277
                return nil, fmt.Errorf("cannot handle file type %s", finfo.Mode())
×
278
        }
279
}
280

281
// FileLsGlob is a rego function that lists the files matching a glob in a directory
282
// in the filesystem being evaluated (which comes from the ingester).
283
// It takes one argument, the path to the pattern to match. It's exposed
284
// as `file.ls_glob`.
285
func FileLsGlob(res *interfaces.Result) func(*rego.Rego) {
56✔
286
        return rego.Function1(
56✔
287
                &rego.Function{
56✔
288
                        Name: "file.ls_glob",
56✔
289
                        Decl: types.NewFunction(types.Args(types.S), types.A),
56✔
290
                },
56✔
291
                fsLsGlob(res.Fs),
56✔
292
        )
56✔
293
}
56✔
294

295
// BaseFileLsGlob is a rego function that lists the files matching a glob
296
// in a directory in the base filesystem being evaluated (in a pull_request
297
// or other diff context).
298
// It takes one argument, the path to the pattern to match. It's exposed
299
// as `base_file.ls_glob`.
300
func BaseFileLsGlob(res *interfaces.Result) func(*rego.Rego) {
1✔
301
        return rego.Function1(
1✔
302
                &rego.Function{
1✔
303
                        Name: "base_file.ls_glob",
1✔
304
                        Decl: types.NewFunction(types.Args(types.S), types.A),
1✔
305
                },
1✔
306
                fsLsGlob(res.BaseFs),
1✔
307
        )
1✔
308
}
1✔
309

310
func fsLsGlob(vfs billy.Filesystem) func(rego.BuiltinContext, *ast.Term) (*ast.Term, error) {
57✔
311
        return func(_ rego.BuiltinContext, op1 *ast.Term) (*ast.Term, error) {
59✔
312
                var path string
2✔
313
                if err := ast.As(op1.Value, &path); err != nil {
2✔
NEW
314
                        return nil, err
×
NEW
315
                }
×
316

317
                if vfs == nil {
2✔
NEW
318
                        return nil, fmt.Errorf("cannot walk file without a filesystem")
×
NEW
319
                }
×
320

321
                matches, err := billyutil.Glob(vfs, path)
2✔
322
                files := []*ast.Term{}
2✔
323

2✔
324
                for _, m := range matches {
6✔
325
                        files = append(files, ast.NewTerm(ast.String(m)))
4✔
326
                }
4✔
327

328
                if err != nil {
2✔
NEW
329
                        return nil, err
×
NEW
330
                }
×
331

332
                return ast.NewTerm(
2✔
333
                        ast.NewArray(files...)), nil
2✔
334
        }
335
}
336

337
// FileWalk is a rego function that walks the files in a directory
338
// in the filesystem being evaluated (which comes from the ingester).
339
// It takes one argument, the path to the directory to walk. It's exposed
340
// as `file.walk`.
341
func FileWalk(res *interfaces.Result) func(*rego.Rego) {
56✔
342
        return rego.Function1(
56✔
343
                &rego.Function{
56✔
344
                        Name: "file.walk",
56✔
345
                        Decl: types.NewFunction(types.Args(types.S), types.A),
56✔
346
                },
56✔
347
                fsWalk(res.Fs),
56✔
348
        )
56✔
349
}
56✔
350

351
// BaseFileWalk is a rego function that walks the files in a directory
352
// in the base filesystem being evaluated (in a pull_request or other
353
// diff context).
354
// It takes one argument, the path to the directory to walk. It's exposed
355
// as `base_file.walk`.
356
func BaseFileWalk(res *interfaces.Result) func(*rego.Rego) {
1✔
357
        return rego.Function1(
1✔
358
                &rego.Function{
1✔
359
                        Name: "base_file.walk",
1✔
360
                        Decl: types.NewFunction(types.Args(types.S), types.A),
1✔
361
                },
1✔
362
                fsWalk(res.BaseFs),
1✔
363
        )
1✔
364
}
1✔
365

366
func fsWalk(vfs billy.Filesystem) func(rego.BuiltinContext, *ast.Term) (*ast.Term, error) {
57✔
367
        return func(_ rego.BuiltinContext, op1 *ast.Term) (*ast.Term, error) {
58✔
368
                var path string
1✔
369
                if err := ast.As(op1.Value, &path); err != nil {
1✔
NEW
370
                        return nil, err
×
NEW
371
                }
×
372

373
                if vfs == nil {
1✔
NEW
374
                        return nil, fmt.Errorf("cannot walk file without a filesystem")
×
NEW
375
                }
×
376

377
                // if the path is a file, return the file itself
378
                // Check file information and return a list of files
379
                // and directories
380
                finfo, err := vfs.Lstat(path)
1✔
381
                if err != nil {
1✔
NEW
382
                        return fileLsHandleError(err)
×
NEW
383
                }
×
384

385
                // If the file is a file return the file itself
386
                if finfo.Mode().IsRegular() {
1✔
NEW
387
                        return fileLsHandleFile(path)
×
NEW
388
                }
×
389

390
                files := []*ast.Term{}
1✔
391
                err = billyutil.Walk(vfs, path, func(path string, info fs.FileInfo, err error) error {
11✔
392
                        // skip if error
10✔
393
                        if err != nil {
10✔
NEW
394
                                return nil
×
395
                        }
×
396

397
                        // skip if directory
398
                        if info.IsDir() {
13✔
399
                                return nil
3✔
400
                        }
3✔
401

402
                        files = append(files, ast.NewTerm(ast.String(path)))
7✔
403
                        return nil
7✔
404
                })
405
                if err != nil {
1✔
NEW
406
                        return nil, err
×
NEW
407
                }
×
408

409
                return ast.NewTerm(
1✔
410
                        ast.NewArray(files...)), nil
1✔
411
        }
412
}
413

414
func fileLsHandleError(err error) (*ast.Term, error) {
1✔
415
        // If the file does not exist return null
1✔
416
        if errors.Is(err, os.ErrNotExist) {
2✔
417
                return ast.NullTerm(), nil
1✔
418
        }
1✔
419
        return nil, err
×
420
}
421

422
func fileLsHandleFile(path string) (*ast.Term, error) {
2✔
423
        return ast.NewTerm(
2✔
424
                ast.NewArray(
2✔
425
                        ast.NewTerm(ast.String(path)),
2✔
426
                ),
2✔
427
        ), nil
2✔
428
}
2✔
429

430
func fileLsHandleDir(path string, bfs billy.Filesystem) (*ast.Term, error) {
4✔
431
        paths, err := bfs.ReadDir(path)
4✔
432
        if err != nil {
4✔
433
                return nil, err
×
434
        }
×
435

436
        var files []*ast.Term
4✔
437
        for _, p := range paths {
11✔
438
                fpath := filepath.Join(path, p.Name())
7✔
439
                files = append(files, ast.NewTerm(ast.String(fpath)))
7✔
440
        }
7✔
441

442
        return ast.NewTerm(
4✔
443
                ast.NewArray(files...)), nil
4✔
444
}
445

446
// FileArchive packages a set of files form the the specified directory into
447
// a tarball.  It takes one argument: a list of file or directory paths to
448
// include, and outputs the tarball as a string.
449
// It's exposed as 'file.archive`.
450
func FileArchive(res *interfaces.Result) func(*rego.Rego) {
1✔
451
        return rego.Function1(
1✔
452
                &rego.Function{
1✔
453
                        Name: "file.archive",
1✔
454
                        Decl: types.NewFunction(types.Args(types.NewArray(nil, types.S)), types.S),
1✔
455
                },
1✔
456
                fsArchive(res.Fs),
1✔
457
        )
1✔
458
}
1✔
459

460
// BaseFileArchive packages a set of files form the the specified directory
461
// in the base filesystem (from a pull_request or other diff context) into
462
// a tarball.  It takes one argument: a list of file or directory paths to
463
// include, and outputs the tarball as a string.
464
// It's exposed as 'base_file.archive`.
465
func BaseFileArchive(res *interfaces.Result) func(*rego.Rego) {
1✔
466
        return rego.Function1(
1✔
467
                &rego.Function{
1✔
468
                        Name: "base_file.archive",
1✔
469
                        Decl: types.NewFunction(types.Args(types.NewArray(nil, types.S)), types.S),
1✔
470
                },
1✔
471
                fsArchive(res.BaseFs),
1✔
472
        )
1✔
473
}
1✔
474

475
func fsArchive(vfs billy.Filesystem) func(rego.BuiltinContext, *ast.Term) (*ast.Term, error) {
2✔
476
        return func(_ rego.BuiltinContext, op1 *ast.Term) (*ast.Term, error) {
3✔
477
                var paths []string
1✔
478
                if err := ast.As(op1.Value, &paths); err != nil {
1✔
NEW
479
                        return nil, err
×
NEW
480
                }
×
481

482
                if vfs == nil {
1✔
NEW
483
                        return nil, fmt.Errorf("cannot archive files without a filesystem")
×
NEW
484
                }
×
485

486
                out := bytes.Buffer{}
1✔
487
                gzWriter := gzip.NewWriter(&out)
1✔
488
                defer gzWriter.Close()
1✔
489
                tarWriter := tar.NewWriter(gzWriter)
1✔
490
                defer tarWriter.Close()
1✔
491

1✔
492
                for _, f := range paths {
3✔
493
                        err := billyutil.Walk(vfs, f, func(path string, info fs.FileInfo, err error) error {
6✔
494
                                if err != nil {
4✔
NEW
495
                                        return err
×
NEW
496
                                }
×
497
                                fileHeader, err := tar.FileInfoHeader(info, "")
4✔
498
                                if err != nil {
4✔
NEW
499
                                        return err
×
NEW
500
                                }
×
501
                                fileHeader.Name = path
4✔
502
                                // memfs doesn't return change times anyway, so zero them for consistency
4✔
503
                                fileHeader.ModTime = time.Time{}
4✔
504
                                fileHeader.AccessTime = time.Time{}
4✔
505
                                fileHeader.ChangeTime = time.Time{}
4✔
506
                                if err := tarWriter.WriteHeader(fileHeader); err != nil {
4✔
NEW
507
                                        return err
×
NEW
508
                                }
×
509
                                if info.Mode().IsRegular() {
7✔
510
                                        file, err := vfs.Open(path)
3✔
511
                                        if err != nil {
3✔
NEW
512
                                                return err
×
NEW
513
                                        }
×
514
                                        defer file.Close()
3✔
515
                                        if _, err := io.Copy(tarWriter, file); err != nil {
3✔
NEW
516
                                                return err
×
NEW
517
                                        }
×
518
                                }
519
                                return nil
4✔
520
                        })
521
                        if err != nil {
2✔
NEW
522
                                return nil, err
×
NEW
523
                        }
×
524
                }
525

526
                if err := tarWriter.Close(); err != nil {
1✔
NEW
527
                        return nil, err
×
NEW
528
                }
×
529
                if err := gzWriter.Close(); err != nil {
1✔
NEW
530
                        return nil, err
×
NEW
531
                }
×
532

533
                return ast.StringTerm(out.String()), nil
1✔
534
        }
535
}
536

537
// ListGithubActions is a rego function that lists the actions in a directory
538
// in the filesystem being evaluated (which comes from the ingester).
539
// It takes one argument, the path to the directory to list. It's exposed
540
// as `github_workflow.ls_actions`.
541
// The function returns a set of strings, each string being the name of an action.
542
// The frizbee library guarantees that the actions are unique.
543
func ListGithubActions(res *interfaces.Result) func(*rego.Rego) {
56✔
544
        return rego.Function1(
56✔
545
                &rego.Function{
56✔
546
                        Name: "github_workflow.ls_actions",
56✔
547
                        Decl: types.NewFunction(types.Args(types.S), types.NewSet(types.S)),
56✔
548
                },
56✔
549
                fsListGithubActions(res.Fs),
56✔
550
        )
56✔
551
}
56✔
552

553
// BaseListGithubActions is a rego function that lists the actions in a directory
554
// in the base filesystem being evaluated (in a pull_request or diff context).
555
// It takes one argument, the path to the directory to list. It's exposed
556
// as `github_workflow.base_ls_actions`.
557
// The function returns a set of strings, each string being the name of an action.
558
// The frizbee library guarantees that the actions are unique.
559
func BaseListGithubActions(res *interfaces.Result) func(*rego.Rego) {
1✔
560
        return rego.Function1(
1✔
561
                &rego.Function{
1✔
562
                        Name: "github_workflow.base_ls_actions",
1✔
563
                        Decl: types.NewFunction(types.Args(types.S), types.NewSet(types.S)),
1✔
564
                },
1✔
565
                fsListGithubActions(res.BaseFs),
1✔
566
        )
1✔
567
}
1✔
568

569
func fsListGithubActions(vfs billy.Filesystem) func(rego.BuiltinContext, *ast.Term) (*ast.Term, error) {
57✔
570
        return func(_ rego.BuiltinContext, op1 *ast.Term) (*ast.Term, error) {
59✔
571
                var base string
2✔
572
                if err := ast.As(op1.Value, &base); err != nil {
2✔
NEW
573
                        return nil, err
×
NEW
574
                }
×
575

576
                if vfs == nil {
2✔
NEW
577
                        return nil, fmt.Errorf("cannot list actions without a filesystem")
×
NEW
578
                }
×
579

580
                var terms []*ast.Term
2✔
581

2✔
582
                // Parse the ingested file system and extract all action references
2✔
583
                r := replacer.NewGitHubActionsReplacer(&config.Config{})
2✔
584
                actions, err := r.ListPathInFS(vfs, base)
2✔
585
                if err != nil {
2✔
NEW
586
                        return nil, err
×
NEW
587
                }
×
588

589
                // Save the action names
590
                for _, a := range actions.Entities {
8✔
591
                        terms = append(terms, ast.StringTerm(a.Name))
6✔
592
                }
6✔
593

594
                return ast.SetTerm(terms...), nil
2✔
595
        }
596
}
597

598
// FileHTTPType is a rego function that returns the HTTP type of a file
599
// in the filesystem being evaluated (which comes from the ingester).
600
// It takes one argument, the path to the file to check. It's exposed
601
// as `file.http_type`.
602
func FileHTTPType(res *interfaces.Result) func(*rego.Rego) {
56✔
603
        return rego.Function1(
56✔
604
                &rego.Function{
56✔
605
                        Name: "file.http_type",
56✔
606
                        Decl: types.NewFunction(types.Args(types.S), types.S),
56✔
607
                },
56✔
608
                fsHTTPType(res.Fs),
56✔
609
        )
56✔
610
}
56✔
611

612
// BaseFileHTTPType is a rego function that returns the HTTP type of a file
613
// in the filesystem being evaluated (which comes from the ingester).
614
// It takes one argument, the path to the file to check. It's exposed
615
// as `base_file.http_type`.
616
func BaseFileHTTPType(res *interfaces.Result) func(*rego.Rego) {
1✔
617
        return rego.Function1(
1✔
618
                &rego.Function{
1✔
619
                        Name: "base_file.http_type",
1✔
620
                        Decl: types.NewFunction(types.Args(types.S), types.S),
1✔
621
                },
1✔
622
                fsHTTPType(res.BaseFs),
1✔
623
        )
1✔
624
}
1✔
625

626
func fsHTTPType(vfs billy.Filesystem) func(rego.BuiltinContext, *ast.Term) (*ast.Term, error) {
57✔
627
        return func(_ rego.BuiltinContext, op1 *ast.Term) (*ast.Term, error) {
59✔
628
                var path string
2✔
629
                if err := ast.As(op1.Value, &path); err != nil {
2✔
NEW
630
                        return nil, err
×
NEW
631
                }
×
632

633
                if vfs == nil {
2✔
NEW
634
                        return nil, fmt.Errorf("cannot list actions without a filesystem")
×
NEW
635
                }
×
636

637
                cpath := filepath.Clean(path)
2✔
638
                f, err := vfs.Open(cpath)
2✔
639
                if err != nil {
2✔
NEW
640
                        return nil, err
×
NEW
641
                }
×
642

643
                defer f.Close()
2✔
644

2✔
645
                buffer := make([]byte, 512)
2✔
646
                n, err := f.Read(buffer)
2✔
647
                if err != nil && err != io.EOF {
2✔
NEW
648
                        return nil, err
×
NEW
649
                }
×
650

651
                httpTyp := http.DetectContentType(buffer[:n])
2✔
652
                astHTTPTyp := ast.String(httpTyp)
2✔
653
                return ast.NewTerm(astHTTPTyp), nil
2✔
654
        }
655
}
656

657
// JQIsTrue is a rego function that accepts parsed YAML data and runs a jq query on it.
658
// The query is a string in jq format that returns a boolean.
659
// It returns a boolean indicating whether the jq query matches the parsed YAML data.
660
// It takes two arguments: the parsed YAML data as an AST term, and the jq query as a string.
661
// It's exposed as `jq.is_true`.
662
func JQIsTrue(_ *interfaces.Result) func(*rego.Rego) {
56✔
663
        return rego.Function2(
56✔
664
                &rego.Function{
56✔
665
                        Name: "jq.is_true",
56✔
666
                        // The function takes two arguments: parsed YAML data and the jq query string
56✔
667
                        Decl: types.NewFunction(types.Args(types.A, types.S), types.B),
56✔
668
                },
56✔
669
                func(_ rego.BuiltinContext, parsedYaml *ast.Term, query *ast.Term) (*ast.Term, error) {
66✔
670
                        var jqQuery string
10✔
671
                        if err := ast.As(query.Value, &jqQuery); err != nil {
10✔
672
                                return nil, err
×
673
                        }
×
674

675
                        // Convert the AST value back to a Go interface{}
676
                        jsonObj, err := ast.JSON(parsedYaml.Value)
10✔
677
                        if err != nil {
10✔
678
                                return nil, fmt.Errorf("error converting AST to JSON: %w", err)
×
679
                        }
×
680

681
                        doesMatch, err := util.JQEvalBoolExpression(context.TODO(), jqQuery, jsonObj)
10✔
682
                        if err != nil {
10✔
683
                                return nil, fmt.Errorf("error running jq query: %w", err)
×
684
                        }
×
685

686
                        return ast.BooleanTerm(doesMatch), nil
10✔
687
                },
688
        )
689
}
690

691
// ParseYaml is a rego function that parses a YAML string into a structured data format.
692
// It takes one argument: the YAML content as a string.
693
// It returns the parsed YAML data as an AST term.
694
// It's exposed as `parse_yaml`.
695
func ParseYaml(_ *interfaces.Result) func(*rego.Rego) {
56✔
696
        return rego.Function1(
56✔
697
                &rego.Function{
56✔
698
                        Name: "parse_yaml",
56✔
699
                        // Takes one string argument (the YAML content) and returns any type
56✔
700
                        Decl: types.NewFunction(types.Args(types.S), types.A),
56✔
701
                },
56✔
702
                func(_ rego.BuiltinContext, yamlContent *ast.Term) (*ast.Term, error) {
73✔
703
                        var yamlStr string
17✔
704

17✔
705
                        // Convert the YAML input from the term into a string
17✔
706
                        if err := ast.As(yamlContent.Value, &yamlStr); err != nil {
17✔
707
                                return nil, err
×
708
                        }
×
709

710
                        // Convert the YAML string into a Go map
711
                        var jsonObj any
17✔
712
                        err := yaml.Unmarshal([]byte(yamlStr), &jsonObj)
17✔
713
                        if err != nil {
18✔
714
                                return nil, fmt.Errorf("error converting YAML to JSON: %w", err)
1✔
715
                        }
1✔
716

717
                        // Convert the Go value to an ast.Value
718
                        value, err := ast.InterfaceToValue(jsonObj)
16✔
719
                        if err != nil {
16✔
720
                                return nil, fmt.Errorf("error converting to AST value: %w", err)
×
721
                        }
×
722

723
                        return ast.NewTerm(value), nil
16✔
724
                },
725
        )
726
}
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