123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- package apiclient
- import (
- "bufio"
- "context"
- "fmt"
- "net/http"
- qs "github.com/google/go-querystring/query"
- "github.com/pkg/errors"
- log "github.com/sirupsen/logrus"
- "github.com/crowdsecurity/go-cs-lib/pkg/ptr"
- "github.com/crowdsecurity/crowdsec/pkg/models"
- "github.com/crowdsecurity/crowdsec/pkg/modelscapi"
- "github.com/crowdsecurity/crowdsec/pkg/types"
- )
- type DecisionsService service
- type DecisionsListOpts struct {
- ScopeEquals *string `url:"scope,omitempty"`
- ValueEquals *string `url:"value,omitempty"`
- TypeEquals *string `url:"type,omitempty"`
- IPEquals *string `url:"ip,omitempty"`
- RangeEquals *string `url:"range,omitempty"`
- Contains *bool `url:"contains,omitempty"`
- ListOpts
- }
- type DecisionsStreamOpts struct {
- Startup bool `url:"startup,omitempty"`
- Scopes string `url:"scopes,omitempty"`
- ScenariosContaining string `url:"scenarios_containing,omitempty"`
- ScenariosNotContaining string `url:"scenarios_not_containing,omitempty"`
- Origins string `url:"origins,omitempty"`
- }
- func (o *DecisionsStreamOpts) addQueryParamsToURL(url string) (string, error) {
- params, err := qs.Values(o)
- if err != nil {
- return "", err
- }
- return fmt.Sprintf("%s?%s", url, params.Encode()), nil
- }
- type DecisionsDeleteOpts struct {
- ScopeEquals *string `url:"scope,omitempty"`
- ValueEquals *string `url:"value,omitempty"`
- TypeEquals *string `url:"type,omitempty"`
- IPEquals *string `url:"ip,omitempty"`
- RangeEquals *string `url:"range,omitempty"`
- Contains *bool `url:"contains,omitempty"`
- OriginEquals *string `url:"origin,omitempty"`
- //
- ScenarioEquals *string `url:"scenario,omitempty"`
- ListOpts
- }
- // to demo query arguments
- func (s *DecisionsService) List(ctx context.Context, opts DecisionsListOpts) (*models.GetDecisionsResponse, *Response, error) {
- var decisions models.GetDecisionsResponse
- params, err := qs.Values(opts)
- if err != nil {
- return nil, nil, err
- }
- u := fmt.Sprintf("%s/decisions?%s", s.client.URLPrefix, params.Encode())
- req, err := s.client.NewRequest(http.MethodGet, u, nil)
- if err != nil {
- return nil, nil, err
- }
- resp, err := s.client.Do(ctx, req, &decisions)
- if err != nil {
- return nil, resp, err
- }
- return &decisions, resp, nil
- }
- func (s *DecisionsService) FetchV2Decisions(ctx context.Context, url string) (*models.DecisionsStreamResponse, *Response, error) {
- var decisions models.DecisionsStreamResponse
- req, err := s.client.NewRequest(http.MethodGet, url, nil)
- if err != nil {
- return nil, nil, err
- }
- resp, err := s.client.Do(ctx, req, &decisions)
- if err != nil {
- return nil, resp, err
- }
- return &decisions, resp, nil
- }
- func (s *DecisionsService) GetDecisionsFromGroups(decisionsGroups []*modelscapi.GetDecisionsStreamResponseNewItem) []*models.Decision {
- var decisions []*models.Decision
- for _, decisionsGroup := range decisionsGroups {
- partialDecisions := make([]*models.Decision, len(decisionsGroup.Decisions))
- for idx, decision := range decisionsGroup.Decisions {
- partialDecisions[idx] = &models.Decision{
- Scenario: decisionsGroup.Scenario,
- Scope: decisionsGroup.Scope,
- Type: ptr.Of(types.DecisionTypeBan),
- Value: decision.Value,
- Duration: decision.Duration,
- Origin: ptr.Of(types.CAPIOrigin),
- }
- }
- decisions = append(decisions, partialDecisions...)
- }
- return decisions
- }
- func (s *DecisionsService) FetchV3Decisions(ctx context.Context, url string) (*models.DecisionsStreamResponse, *Response, error) {
- var decisions modelscapi.GetDecisionsStreamResponse
- var v2Decisions models.DecisionsStreamResponse
- scenarioDeleted := "deleted"
- durationDeleted := "1h"
- req, err := s.client.NewRequest(http.MethodGet, url, nil)
- if err != nil {
- return nil, nil, err
- }
- resp, err := s.client.Do(ctx, req, &decisions)
- if err != nil {
- return nil, resp, err
- }
- v2Decisions.New = s.GetDecisionsFromGroups(decisions.New)
- for _, decisionsGroup := range decisions.Deleted {
- partialDecisions := make([]*models.Decision, len(decisionsGroup.Decisions))
- for idx, decision := range decisionsGroup.Decisions {
- decision := decision // fix exportloopref linter message
- partialDecisions[idx] = &models.Decision{
- Scenario: &scenarioDeleted,
- Scope: decisionsGroup.Scope,
- Type: ptr.Of(types.DecisionTypeBan),
- Value: &decision,
- Duration: &durationDeleted,
- Origin: ptr.Of(types.CAPIOrigin),
- }
- }
- v2Decisions.Deleted = append(v2Decisions.Deleted, partialDecisions...)
- }
- return &v2Decisions, resp, nil
- }
- func (s *DecisionsService) GetDecisionsFromBlocklist(ctx context.Context, blocklist *modelscapi.BlocklistLink, lastPullTimestamp *string) ([]*models.Decision, bool, error) {
- if blocklist.URL == nil {
- return nil, false, errors.New("blocklist URL is nil")
- }
- log.Debugf("Fetching blocklist %s", *blocklist.URL)
- client := http.Client{}
- req, err := http.NewRequest(http.MethodGet, *blocklist.URL, nil)
- if err != nil {
- return nil, false, err
- }
- if lastPullTimestamp != nil {
- req.Header.Set("If-Modified-Since", *lastPullTimestamp)
- }
- req = req.WithContext(ctx)
- log.Debugf("[URL] %s %s", req.Method, req.URL)
- // we dont use client_http Do method because we need the reader and is not provided. We would be forced to use Pipe and goroutine, etc
- resp, err := client.Do(req)
- if resp != nil && resp.Body != nil {
- defer resp.Body.Close()
- }
- if err != nil {
- // If we got an error, and the context has been canceled,
- // the context's error is probably more useful.
- select {
- case <-ctx.Done():
- return nil, false, ctx.Err()
- default:
- }
- // If the error type is *url.Error, sanitize its URL before returning.
- log.Errorf("Error fetching blocklist %s: %s", *blocklist.URL, err)
- return nil, false, err
- }
- if resp.StatusCode == http.StatusNotModified {
- if lastPullTimestamp != nil {
- log.Debugf("Blocklist %s has not been modified since %s", *blocklist.URL, *lastPullTimestamp)
- } else {
- log.Debugf("Blocklist %s has not been modified (decisions about to expire)", *blocklist.URL)
- }
- return nil, false, nil
- }
- if resp.StatusCode != http.StatusOK {
- log.Debugf("Received nok status code %d for blocklist %s", resp.StatusCode, *blocklist.URL)
- return nil, false, nil
- }
- decisions := make([]*models.Decision, 0)
- scanner := bufio.NewScanner(resp.Body)
- for scanner.Scan() {
- decision := scanner.Text()
- decisions = append(decisions, &models.Decision{
- Scenario: blocklist.Name,
- Scope: blocklist.Scope,
- Type: blocklist.Remediation,
- Value: &decision,
- Duration: blocklist.Duration,
- Origin: ptr.Of(types.ListOrigin),
- })
- }
- // here the upper go routine is finished because scanner.Scan() is blocking until pw.Close() is called
- // so it's safe to use the isModified variable here
- return decisions, true, nil
- }
- func (s *DecisionsService) GetStream(ctx context.Context, opts DecisionsStreamOpts) (*models.DecisionsStreamResponse, *Response, error) {
- u, err := opts.addQueryParamsToURL(s.client.URLPrefix + "/decisions/stream")
- if err != nil {
- return nil, nil, err
- }
- if s.client.URLPrefix == "v3" {
- return s.FetchV3Decisions(ctx, u)
- } else {
- return s.FetchV2Decisions(ctx, u)
- }
- }
- func (s *DecisionsService) GetStreamV3(ctx context.Context, opts DecisionsStreamOpts) (*modelscapi.GetDecisionsStreamResponse, *Response, error) {
- u, err := opts.addQueryParamsToURL(s.client.URLPrefix + "/decisions/stream")
- if err != nil {
- return nil, nil, err
- }
- var decisions modelscapi.GetDecisionsStreamResponse
- req, err := s.client.NewRequest(http.MethodGet, u, nil)
- if err != nil {
- return nil, nil, err
- }
- resp, err := s.client.Do(ctx, req, &decisions)
- if err != nil {
- return nil, resp, err
- }
- return &decisions, resp, nil
- }
- func (s *DecisionsService) StopStream(ctx context.Context) (*Response, error) {
- u := fmt.Sprintf("%s/decisions", s.client.URLPrefix)
- req, err := s.client.NewRequest(http.MethodDelete, u, nil)
- if err != nil {
- return nil, err
- }
- resp, err := s.client.Do(ctx, req, nil)
- if err != nil {
- return resp, err
- }
- return resp, nil
- }
- func (s *DecisionsService) Delete(ctx context.Context, opts DecisionsDeleteOpts) (*models.DeleteDecisionResponse, *Response, error) {
- var deleteDecisionResponse models.DeleteDecisionResponse
- params, err := qs.Values(opts)
- if err != nil {
- return nil, nil, err
- }
- u := fmt.Sprintf("%s/decisions?%s", s.client.URLPrefix, params.Encode())
- req, err := s.client.NewRequest(http.MethodDelete, u, nil)
- if err != nil {
- return nil, nil, err
- }
- resp, err := s.client.Do(ctx, req, &deleteDecisionResponse)
- if err != nil {
- return nil, resp, err
- }
- return &deleteDecisionResponse, resp, nil
- }
- func (s *DecisionsService) DeleteOne(ctx context.Context, decision_id string) (*models.DeleteDecisionResponse, *Response, error) {
- var deleteDecisionResponse models.DeleteDecisionResponse
- u := fmt.Sprintf("%s/decisions/%s", s.client.URLPrefix, decision_id)
- req, err := s.client.NewRequest(http.MethodDelete, u, nil)
- if err != nil {
- return nil, nil, err
- }
- resp, err := s.client.Do(ctx, req, &deleteDecisionResponse)
- if err != nil {
- return nil, resp, err
- }
- return &deleteDecisionResponse, resp, nil
- }
|