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  }