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

mindersec / minder / 15982590714

30 Jun 2025 08:07PM UTC coverage: 57.402% (+0.01%) from 57.392%
15982590714

Pull #5702

github

web-flow
Merge e8afcef43 into 9b4f171a0
Pull Request #5702: Add templates for REST ingest and remediate, and YQ remediation

63 of 78 new or added lines in 7 files covered. (80.77%)

3 existing lines in 2 files now uncovered.

18596 of 32396 relevant lines covered (57.4%)

37.21 hits per line

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

74.11
/internal/engine/actions/remediate/rest/rest.go
1
// SPDX-FileCopyrightText: Copyright 2023 The Minder Authors
2
// SPDX-License-Identifier: Apache-2.0
3

4
// Package rest provides the REST remediation engine
5
package rest
6

7
import (
8
        "bytes"
9
        "cmp"
10
        "context"
11
        "encoding/json"
12
        "errors"
13
        "fmt"
14
        "net/http"
15
        "strings"
16

17
        "github.com/google/go-github/v63/github"
18
        "github.com/rs/zerolog"
19
        "github.com/rs/zerolog/log"
20
        "google.golang.org/protobuf/reflect/protoreflect"
21

22
        engerrors "github.com/mindersec/minder/internal/engine/errors"
23
        "github.com/mindersec/minder/internal/engine/interfaces"
24
        "github.com/mindersec/minder/internal/util"
25
        pb "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
26
        "github.com/mindersec/minder/pkg/profiles/models"
27
        provifv1 "github.com/mindersec/minder/pkg/providers/v1"
28
)
29

30
const (
31
        // RemediateType is the type of the REST remediation engine
32
        RemediateType = "rest"
33

34
        // methodBytesLimit is the maximum number of bytes for the HTTP method
35
        methodBytesLimit = 10
36

37
        // endpointBytesLimit is the maximum number of bytes for the endpoint
38
        endpointBytesLimit = 1024
39

40
        // bodyBytesLimit is the maximum number of bytes for the body
41
        bodyBytesLimit = 5120
42
)
43

44
// Remediator keeps the status for a rule type that uses REST remediation
45
type Remediator struct {
46
        actionType       interfaces.ActionType
47
        method           *util.SafeTemplate
48
        cli              provifv1.REST
49
        endpointTemplate *util.SafeTemplate
50
        bodyTemplate     *util.SafeTemplate
51
        // Setting defines the current action setting. e.g. dry-run, on, off
52
        setting models.ActionOpt
53
}
54

55
// NewRestRemediate creates a new REST rule data ingest engine
56
func NewRestRemediate(
57
        actionType interfaces.ActionType, restCfg *pb.RestType, cli provifv1.REST,
58
        setting models.ActionOpt,
59
) (*Remediator, error) {
14✔
60
        if actionType == "" {
15✔
61
                return nil, fmt.Errorf("action type cannot be empty")
1✔
62
        }
1✔
63

64
        endpointTmpl, err := util.NewSafeTextTemplate(&restCfg.Endpoint, "endpoint")
13✔
65
        if err != nil {
14✔
66
                return nil, fmt.Errorf("invalid endpoint: %w", err)
1✔
67
        }
1✔
68

69
        var bodyTmpl *util.SafeTemplate
12✔
70
        if restCfg.Body != nil {
23✔
71
                bodyTmpl, err = util.NewSafeTextTemplate(restCfg.Body, "body")
11✔
72
                if err != nil {
12✔
73
                        return nil, fmt.Errorf("invalid body: %w", err)
1✔
74
                }
1✔
75
        }
76

77
        methodStr := cmp.Or(restCfg.Method, http.MethodPatch)
11✔
78
        methodTemplate, err := util.NewSafeTextTemplate(&methodStr, "method")
11✔
79
        if err != nil {
12✔
80
                return nil, fmt.Errorf("invalid method: %w", err)
1✔
81
        }
1✔
82

83
        return &Remediator{
10✔
84
                cli:              cli,
10✔
85
                actionType:       actionType,
10✔
86
                method:           methodTemplate,
10✔
87
                endpointTemplate: endpointTmpl,
10✔
88
                bodyTemplate:     bodyTmpl,
10✔
89
                setting:          setting,
10✔
90
        }, nil
10✔
91
}
92

93
// EndpointTemplateParams is the parameters for the REST endpoint template
94
type EndpointTemplateParams struct {
95
        // Entity is the entity to be evaluated
96
        Entity any
97
        // Profile is the parameters to be used in the template
98
        Profile map[string]any
99
        // Params are the rule instance parameters
100
        Params map[string]any
101
        // EvalResultOutput is the output from the rule evaluation
102
        EvalResultOutput any
103
}
104

105
// Class returns the action type of the remediation engine
106
func (r *Remediator) Class() interfaces.ActionType {
×
107
        return r.actionType
×
108
}
×
109

110
// Type returns the action subtype of the remediation engine
111
func (*Remediator) Type() string {
×
112
        return RemediateType
×
113
}
×
114

115
// GetOnOffState returns the alert action state read from the profile
116
func (r *Remediator) GetOnOffState() models.ActionOpt {
×
117
        return models.ActionOptOrDefault(r.setting, models.ActionOptOff)
×
118
}
×
119

120
// Do perform the remediation
121
func (r *Remediator) Do(
122
        ctx context.Context,
123
        cmd interfaces.ActionCmd,
124
        entity protoreflect.ProtoMessage,
125
        params interfaces.ActionsParams,
126
        _ *json.RawMessage,
127
) (json.RawMessage, error) {
7✔
128
        // Remediating through rest doesn't really have a turn-off behavior so
7✔
129
        // only proceed with the remediation if the command is to turn on the action
7✔
130
        if cmd != interfaces.ActionCmdOn {
7✔
131
                return nil, engerrors.ErrActionSkipped
×
132
        }
×
133

134
        retp := &EndpointTemplateParams{
7✔
135
                Entity:  entity,
7✔
136
                Profile: params.GetRule().Def,
7✔
137
                Params:  params.GetRule().Params,
7✔
138
        }
7✔
139
        if params.GetEvalResult() != nil {
8✔
140
                retp.EvalResultOutput = params.GetEvalResult().Output
1✔
141
        }
1✔
142

143
        method := new(bytes.Buffer)
7✔
144
        if err := r.method.Execute(ctx, method, retp, methodBytesLimit); err != nil {
7✔
NEW
145
                return nil, fmt.Errorf("cannot execute method template: %w", err)
×
NEW
146
        }
×
147

148
        endpoint := new(bytes.Buffer)
7✔
149
        if err := r.endpointTemplate.Execute(ctx, endpoint, retp, endpointBytesLimit); err != nil {
7✔
150
                return nil, fmt.Errorf("cannot execute endpoint template: %w", err)
×
151
        }
×
152

153
        body := new(bytes.Buffer)
7✔
154
        if r.bodyTemplate != nil {
13✔
155
                if err := r.bodyTemplate.Execute(ctx, body, retp, bodyBytesLimit); err != nil {
6✔
156
                        return nil, fmt.Errorf("cannot execute endpoint template: %w", err)
×
157
                }
×
158
        }
159

160
        zerolog.Ctx(ctx).Debug().
7✔
161
                Msgf("remediating with endpoint: [%s] and body [%+v]", endpoint.String(), body.String())
7✔
162

7✔
163
        var err error
7✔
164
        switch r.setting {
7✔
165
        case models.ActionOptOn:
5✔
166
                err = r.run(ctx, method.String(), endpoint.String(), body.Bytes())
5✔
167
        case models.ActionOptDryRun:
1✔
168
                err = r.dryRun(ctx, method.String(), endpoint.String(), body.String())
1✔
169
        case models.ActionOptOff, models.ActionOptUnknown:
1✔
170
                err = errors.New("unexpected action")
1✔
171
        }
172
        return nil, err
7✔
173
}
174

175
func (r *Remediator) run(ctx context.Context, method string, endpoint string, body []byte) error {
5✔
176
        // create an empty map, not a nil map to avoid passing nil to NewRequest
5✔
177
        bodyJson := make(map[string]any)
5✔
178

5✔
179
        if len(body) > 0 {
9✔
180
                err := json.Unmarshal(body, &bodyJson)
4✔
181
                if err != nil {
4✔
182
                        return fmt.Errorf("cannot unmarshal body: %w", err)
×
183
                }
×
184
        }
185

186
        req, err := r.cli.NewRequest(strings.ToUpper(method), endpoint, bodyJson)
5✔
187
        if err != nil {
5✔
188
                return fmt.Errorf("cannot create request: %w", err)
×
189
        }
×
190

191
        resp, err := r.cli.Do(ctx, req)
5✔
192
        if err != nil {
6✔
193
                var respErr *github.ErrorResponse
1✔
194
                if errors.As(err, &respErr) {
2✔
195
                        zerolog.Ctx(ctx).Error().Msgf("Error message: %v", respErr.Message)
1✔
196
                        for _, e := range respErr.Errors {
1✔
197
                                zerolog.Ctx(ctx).Error().Msgf("Field: %s, Message: %s", e.Field, e.Message)
×
198
                        }
×
199
                }
200
                return fmt.Errorf("cannot make request: %w", err)
1✔
201
        }
202

203
        defer func() {
8✔
204
                if err := resp.Body.Close(); err != nil {
4✔
205
                        log.Printf("cannot close response body: %v", err)
×
206
                }
×
207
        }()
208
        // Translate the http status code response to an error
209
        if engerrors.HTTPErrorCodeToErr(resp.StatusCode) != nil {
4✔
210
                return engerrors.NewErrActionFailed("remediation failed: %s", err)
×
211
        }
×
212
        return nil
4✔
213
}
214

215
func (r *Remediator) dryRun(ctx context.Context, method, endpoint, body string) error {
1✔
216
        curlCmd, err := util.GenerateCurlCommand(ctx, method, r.cli.GetBaseURL(), endpoint, body)
1✔
217
        if err != nil {
1✔
218
                return fmt.Errorf("cannot generate curl command: %w", err)
×
219
        }
×
220

221
        log.Printf("run the following curl command: \n%s\n", curlCmd)
1✔
222
        return nil
1✔
223
}
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

© 2026 Coveralls, Inc