github.com/crowdsecurity/crowdsec@v1.6.1/pkg/apiclient/decisions_service.go (about) 1 package apiclient 2 3 import ( 4 "bufio" 5 "context" 6 "errors" 7 "fmt" 8 "net/http" 9 10 qs "github.com/google/go-querystring/query" 11 log "github.com/sirupsen/logrus" 12 13 "github.com/crowdsecurity/go-cs-lib/ptr" 14 15 "github.com/crowdsecurity/crowdsec/pkg/models" 16 "github.com/crowdsecurity/crowdsec/pkg/modelscapi" 17 "github.com/crowdsecurity/crowdsec/pkg/types" 18 ) 19 20 type DecisionsService service 21 22 type DecisionsListOpts struct { 23 ScopeEquals *string `url:"scope,omitempty"` 24 ValueEquals *string `url:"value,omitempty"` 25 TypeEquals *string `url:"type,omitempty"` 26 IPEquals *string `url:"ip,omitempty"` 27 RangeEquals *string `url:"range,omitempty"` 28 Contains *bool `url:"contains,omitempty"` 29 ListOpts 30 } 31 32 type DecisionsStreamOpts struct { 33 Startup bool `url:"startup,omitempty"` 34 Scopes string `url:"scopes,omitempty"` 35 ScenariosContaining string `url:"scenarios_containing,omitempty"` 36 ScenariosNotContaining string `url:"scenarios_not_containing,omitempty"` 37 Origins string `url:"origins,omitempty"` 38 } 39 40 func (o *DecisionsStreamOpts) addQueryParamsToURL(url string) (string, error) { 41 params, err := qs.Values(o) 42 if err != nil { 43 return "", err 44 } 45 46 return fmt.Sprintf("%s?%s", url, params.Encode()), nil 47 } 48 49 type DecisionsDeleteOpts struct { 50 ScopeEquals *string `url:"scope,omitempty"` 51 ValueEquals *string `url:"value,omitempty"` 52 TypeEquals *string `url:"type,omitempty"` 53 IPEquals *string `url:"ip,omitempty"` 54 RangeEquals *string `url:"range,omitempty"` 55 Contains *bool `url:"contains,omitempty"` 56 OriginEquals *string `url:"origin,omitempty"` 57 // 58 ScenarioEquals *string `url:"scenario,omitempty"` 59 ListOpts 60 } 61 62 // to demo query arguments 63 func (s *DecisionsService) List(ctx context.Context, opts DecisionsListOpts) (*models.GetDecisionsResponse, *Response, error) { 64 params, err := qs.Values(opts) 65 if err != nil { 66 return nil, nil, err 67 } 68 69 u := fmt.Sprintf("%s/decisions?%s", s.client.URLPrefix, params.Encode()) 70 71 req, err := s.client.NewRequest(http.MethodGet, u, nil) 72 if err != nil { 73 return nil, nil, err 74 } 75 76 var decisions models.GetDecisionsResponse 77 78 resp, err := s.client.Do(ctx, req, &decisions) 79 if err != nil { 80 return nil, resp, err 81 } 82 83 return &decisions, resp, nil 84 } 85 86 func (s *DecisionsService) FetchV2Decisions(ctx context.Context, url string) (*models.DecisionsStreamResponse, *Response, error) { 87 req, err := s.client.NewRequest(http.MethodGet, url, nil) 88 if err != nil { 89 return nil, nil, err 90 } 91 92 var decisions models.DecisionsStreamResponse 93 94 resp, err := s.client.Do(ctx, req, &decisions) 95 if err != nil { 96 return nil, resp, err 97 } 98 99 return &decisions, resp, nil 100 } 101 102 func (s *DecisionsService) GetDecisionsFromGroups(decisionsGroups []*modelscapi.GetDecisionsStreamResponseNewItem) []*models.Decision { 103 decisions := make([]*models.Decision, 0) 104 105 for _, decisionsGroup := range decisionsGroups { 106 partialDecisions := make([]*models.Decision, len(decisionsGroup.Decisions)) 107 for idx, decision := range decisionsGroup.Decisions { 108 partialDecisions[idx] = &models.Decision{ 109 Scenario: decisionsGroup.Scenario, 110 Scope: decisionsGroup.Scope, 111 Type: ptr.Of(types.DecisionTypeBan), 112 Value: decision.Value, 113 Duration: decision.Duration, 114 Origin: ptr.Of(types.CAPIOrigin), 115 } 116 } 117 118 decisions = append(decisions, partialDecisions...) 119 } 120 121 return decisions 122 } 123 124 func (s *DecisionsService) FetchV3Decisions(ctx context.Context, url string) (*models.DecisionsStreamResponse, *Response, error) { 125 scenarioDeleted := "deleted" 126 durationDeleted := "1h" 127 128 req, err := s.client.NewRequest(http.MethodGet, url, nil) 129 if err != nil { 130 return nil, nil, err 131 } 132 133 decisions := modelscapi.GetDecisionsStreamResponse{} 134 135 resp, err := s.client.Do(ctx, req, &decisions) 136 if err != nil { 137 return nil, resp, err 138 } 139 140 v2Decisions := models.DecisionsStreamResponse{} 141 v2Decisions.New = s.GetDecisionsFromGroups(decisions.New) 142 143 for _, decisionsGroup := range decisions.Deleted { 144 partialDecisions := make([]*models.Decision, len(decisionsGroup.Decisions)) 145 146 for idx, decision := range decisionsGroup.Decisions { 147 decision := decision // fix exportloopref linter message 148 partialDecisions[idx] = &models.Decision{ 149 Scenario: &scenarioDeleted, 150 Scope: decisionsGroup.Scope, 151 Type: ptr.Of(types.DecisionTypeBan), 152 Value: &decision, 153 Duration: &durationDeleted, 154 Origin: ptr.Of(types.CAPIOrigin), 155 } 156 } 157 158 v2Decisions.Deleted = append(v2Decisions.Deleted, partialDecisions...) 159 } 160 161 return &v2Decisions, resp, nil 162 } 163 164 func (s *DecisionsService) GetDecisionsFromBlocklist(ctx context.Context, blocklist *modelscapi.BlocklistLink, lastPullTimestamp *string) ([]*models.Decision, bool, error) { 165 if blocklist.URL == nil { 166 return nil, false, errors.New("blocklist URL is nil") 167 } 168 169 log.Debugf("Fetching blocklist %s", *blocklist.URL) 170 171 client := http.Client{} 172 173 req, err := http.NewRequest(http.MethodGet, *blocklist.URL, nil) 174 if err != nil { 175 return nil, false, err 176 } 177 178 if lastPullTimestamp != nil { 179 req.Header.Set("If-Modified-Since", *lastPullTimestamp) 180 } 181 182 req = req.WithContext(ctx) 183 log.Debugf("[URL] %s %s", req.Method, req.URL) 184 185 // we don't use client_http Do method because we need the reader and is not provided. 186 // We would be forced to use Pipe and goroutine, etc 187 resp, err := client.Do(req) 188 if resp != nil && resp.Body != nil { 189 defer resp.Body.Close() 190 } 191 192 if err != nil { 193 // If we got an error, and the context has been canceled, 194 // the context's error is probably more useful. 195 select { 196 case <-ctx.Done(): 197 return nil, false, ctx.Err() 198 default: 199 } 200 201 // If the error type is *url.Error, sanitize its URL before returning. 202 log.Errorf("Error fetching blocklist %s: %s", *blocklist.URL, err) 203 204 return nil, false, err 205 } 206 207 if resp.StatusCode == http.StatusNotModified { 208 if lastPullTimestamp != nil { 209 log.Debugf("Blocklist %s has not been modified since %s", *blocklist.URL, *lastPullTimestamp) 210 } else { 211 log.Debugf("Blocklist %s has not been modified (decisions about to expire)", *blocklist.URL) 212 } 213 214 return nil, false, nil 215 } 216 217 if resp.StatusCode != http.StatusOK { 218 log.Debugf("Received nok status code %d for blocklist %s", resp.StatusCode, *blocklist.URL) 219 220 return nil, false, nil 221 } 222 223 decisions := make([]*models.Decision, 0) 224 225 scanner := bufio.NewScanner(resp.Body) 226 for scanner.Scan() { 227 decision := scanner.Text() 228 decisions = append(decisions, &models.Decision{ 229 Scenario: blocklist.Name, 230 Scope: blocklist.Scope, 231 Type: blocklist.Remediation, 232 Value: &decision, 233 Duration: blocklist.Duration, 234 Origin: ptr.Of(types.ListOrigin), 235 }) 236 } 237 238 // here the upper go routine is finished because scanner.Scan() is blocking until pw.Close() is called 239 // so it's safe to use the isModified variable here 240 return decisions, true, nil 241 } 242 243 func (s *DecisionsService) GetStream(ctx context.Context, opts DecisionsStreamOpts) (*models.DecisionsStreamResponse, *Response, error) { 244 u, err := opts.addQueryParamsToURL(s.client.URLPrefix + "/decisions/stream") 245 if err != nil { 246 return nil, nil, err 247 } 248 249 if s.client.URLPrefix != "v3" { 250 return s.FetchV2Decisions(ctx, u) 251 } 252 253 return s.FetchV3Decisions(ctx, u) 254 } 255 256 func (s *DecisionsService) GetStreamV3(ctx context.Context, opts DecisionsStreamOpts) (*modelscapi.GetDecisionsStreamResponse, *Response, error) { 257 u, err := opts.addQueryParamsToURL(s.client.URLPrefix + "/decisions/stream") 258 if err != nil { 259 return nil, nil, err 260 } 261 262 req, err := s.client.NewRequest(http.MethodGet, u, nil) 263 if err != nil { 264 return nil, nil, err 265 } 266 267 decisions := modelscapi.GetDecisionsStreamResponse{} 268 269 resp, err := s.client.Do(ctx, req, &decisions) 270 if err != nil { 271 return nil, resp, err 272 } 273 274 return &decisions, resp, nil 275 } 276 277 func (s *DecisionsService) StopStream(ctx context.Context) (*Response, error) { 278 u := fmt.Sprintf("%s/decisions", s.client.URLPrefix) 279 280 req, err := s.client.NewRequest(http.MethodDelete, u, nil) 281 if err != nil { 282 return nil, err 283 } 284 285 resp, err := s.client.Do(ctx, req, nil) 286 if err != nil { 287 return resp, err 288 } 289 290 return resp, nil 291 } 292 293 func (s *DecisionsService) Delete(ctx context.Context, opts DecisionsDeleteOpts) (*models.DeleteDecisionResponse, *Response, error) { 294 params, err := qs.Values(opts) 295 if err != nil { 296 return nil, nil, err 297 } 298 299 u := fmt.Sprintf("%s/decisions?%s", s.client.URLPrefix, params.Encode()) 300 301 req, err := s.client.NewRequest(http.MethodDelete, u, nil) 302 if err != nil { 303 return nil, nil, err 304 } 305 306 deleteDecisionResponse := models.DeleteDecisionResponse{} 307 308 resp, err := s.client.Do(ctx, req, &deleteDecisionResponse) 309 if err != nil { 310 return nil, resp, err 311 } 312 313 return &deleteDecisionResponse, resp, nil 314 } 315 316 func (s *DecisionsService) DeleteOne(ctx context.Context, decisionID string) (*models.DeleteDecisionResponse, *Response, error) { 317 u := fmt.Sprintf("%s/decisions/%s", s.client.URLPrefix, decisionID) 318 319 req, err := s.client.NewRequest(http.MethodDelete, u, nil) 320 if err != nil { 321 return nil, nil, err 322 } 323 324 deleteDecisionResponse := models.DeleteDecisionResponse{} 325 326 resp, err := s.client.Do(ctx, req, &deleteDecisionResponse) 327 if err != nil { 328 return nil, resp, err 329 } 330 331 return &deleteDecisionResponse, resp, nil 332 }