Apiclient tests (#484)

Co-authored-by: AlteredCoder
Co-authored-by: erenJag
This commit is contained in:
Thibault "bui" Koechlin 2020-11-30 16:15:07 +01:00 committed by GitHub
parent 000fec27df
commit 71ac0d2fce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1385 additions and 74 deletions

View file

@ -1,30 +0,0 @@
# Contributing
You have an idea, a suggestion or you spotted a mistake ?
Help us improve the software and the user experience, to make the internet a safer place together !
## Contributing to the documentation
If you spotted some mistakes in the documentation or have improvement suggestions, you can :
- open a {{v0X.doc.new_issue}} if you are comfortable with github
- let us know on {{v0X.doc.discourse}} if you want to discuss about it
Let us as well know if you have some improvement suggestions !
## Contributing to the code
- If you want to report a bug, you can use [the github bugtracker]({{v0X.crowdsec.bugreport}})
- If you want to suggest an improvement you can use either [the github bugtracker]({{v0X.crowdsec.bugreport}}) or the {{v0X.doc.discourse}} if you want to discuss
## Contributing to the parsers/scenarios
If you want to contribute your parser or scenario to the community and have them appear on the {{v0X.hub.htmlname}}, you should [open a merge request](https://github.com/crowdsecurity/hub/pulls) on the hub.
We are currently working on a proper [CI](https://en.wikipedia.org/wiki/Continuous_integration) for the {{v0X.hub.htmlname}}, so for now all contribution are subject to peer-review, please bear with us !

View file

@ -37,7 +37,6 @@ nav:
- Expressions: write_configurations/expressions.md - Expressions: write_configurations/expressions.md
- bouncers: bouncers/index.md - bouncers: bouncers/index.md
- Contributing: - Contributing:
- General: contributing.md
- Writing Output Plugins: references/plugins_api.md - Writing Output Plugins: references/plugins_api.md
- Cscli commands: - Cscli commands:
- Cscli: cscli/cscli.md - Cscli: cscli/cscli.md

View file

@ -1,3 +0,0 @@
# Crowdsec
{{macros_info() }}

View file

@ -75,10 +75,5 @@ nav:
- Admin Guide: - Admin Guide:
- Services Configuration: admin_guide/services_configuration.md - Services Configuration: admin_guide/services_configuration.md
- Architecture: admin_guide/architecture.md - Architecture: admin_guide/architecture.md
- Contributing:
- General: contributing.md
- Reporting bugs: contributing.md
- Asking questions: contributing.md
- Publishing parsers & scenarios: contributing.md
- Upgrade V0.X to V1.X: migration.md - Upgrade V0.X to V1.X: migration.md

View file

@ -6,6 +6,7 @@ nav:
- Developers : https://crowdsecurity.github.io/api_doc/index.html?urls.primaryName=LAPI" target="_blank - Developers : https://crowdsecurity.github.io/api_doc/index.html?urls.primaryName=LAPI" target="_blank
- Hub : https://hub.crowdsec.net/" target="_blank - Hub : https://hub.crowdsec.net/" target="_blank
- Releases : https://github.com/crowdsecurity/crowdsec/releases" target="_blank - Releases : https://github.com/crowdsecurity/crowdsec/releases" target="_blank
- Contributing: contributing.md
- FAQ: faq.md - FAQ: faq.md
@ -42,7 +43,7 @@ google_analytics:
- auto - auto
extra: extra:
swagger_url: "https://raw.githubusercontent.com/crowdsecurity/crowdsec/wip_lapi/pkg/models/localapi_swagger.yaml" swagger_url: "https://raw.githubusercontent.com/crowdsecurity/crowdsec/master/pkg/models/localapi_swagger.yaml"
v0X: v0X:
doc: doc:
new_issue: "[new documentation issue](https://github.com/crowdsecurity/crowdsec/issues/new)" new_issue: "[new documentation issue](https://github.com/crowdsecurity/crowdsec/issues/new)"

View file

@ -6,6 +6,7 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/models" "github.com/crowdsecurity/crowdsec/pkg/models"
qs "github.com/google/go-querystring/query" qs "github.com/google/go-querystring/query"
"github.com/pkg/errors"
) )
// type ApiAlerts service // type ApiAlerts service
@ -65,7 +66,7 @@ func (s *AlertsService) List(ctx context.Context, opts AlertsListOpts) (*models.
u := fmt.Sprintf("%s/alerts", s.client.URLPrefix) u := fmt.Sprintf("%s/alerts", s.client.URLPrefix)
params, err := qs.Values(opts) params, err := qs.Values(opts)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, errors.Wrap(err, "building query")
} }
if len(params) > 0 { if len(params) > 0 {
URI = fmt.Sprintf("%s?%s", u, params.Encode()) URI = fmt.Sprintf("%s?%s", u, params.Encode())
@ -75,12 +76,12 @@ func (s *AlertsService) List(ctx context.Context, opts AlertsListOpts) (*models.
req, err := s.client.NewRequest("GET", URI, nil) req, err := s.client.NewRequest("GET", URI, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, errors.Wrap(err, "building request")
} }
resp, err := s.client.Do(ctx, req, &alerts) resp, err := s.client.Do(ctx, req, &alerts)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, errors.Wrap(err, "performing request")
} }
return &alerts, resp, nil return &alerts, resp, nil
} }
@ -117,7 +118,7 @@ func (s *AlertsService) GetByID(ctx context.Context, alertID int) (*models.Alert
resp, err := s.client.Do(ctx, req, &alert) resp, err := s.client.Do(ctx, req, &alert)
if err != nil { if err != nil {
return nil, resp, err return nil, nil, err
} }
return &alert, resp, nil return &alert, resp, nil
} }

View file

@ -0,0 +1,496 @@
package apiclient
import (
"context"
"fmt"
"net/http"
"net/url"
"reflect"
"testing"
"github.com/crowdsecurity/crowdsec/pkg/cwversion"
"github.com/crowdsecurity/crowdsec/pkg/models"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func TestAlertsListAsMachine(t *testing.T) {
log.SetLevel(log.DebugLevel)
mux, urlx, teardown := setup()
mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"code": 200, "expire": "2030-01-02T15:04:05Z", "token": "oklol"}`))
})
log.Printf("URL is %s", urlx)
apiURL, err := url.Parse(urlx + "/")
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
client, err := NewClient(&Config{
MachineID: "test_login",
Password: "test_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
})
if err != nil {
log.Fatalf("new api client: %s", err.Error())
}
defer teardown()
mux.HandleFunc("/alerts", func(w http.ResponseWriter, r *http.Request) {
if r.URL.RawQuery == "ip=1.2.3.4" {
testMethod(t, r, "GET")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `null`)
return
}
testMethod(t, r, "GET")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `[
{"capacity":5,"created_at":"2020-11-28T10:20:47+01:00",
"decisions":[
{"duration":"59m49.264032632s",
"end_ip":16843180,
"id":1,
"origin":"crowdsec",
"scenario":"crowdsecurity/ssh-bf",
"scope":"Ip",
"simulated":false,
"start_ip":16843180,
"type":"ban",
"value":"1.1.1.172"}
],
"events":[
{"meta":[
{"key":"target_user","value":"netflix"},
{"key":"service","value":"ssh"}
],
"timestamp":"2020-11-28 10:20:46 +0000 UTC"},
{"meta":[
{"key":"target_user","value":"netflix"},
{"key":"service","value":"ssh"}
],
"timestamp":"2020-11-28 10:20:46 +0000 UTC"}
],
"events_count":6,
"id":1,
"labels":null,
"leakspeed":"10s",
"machine_id":"test",
"message":"Ip 1.1.1.172 performed 'crowdsecurity/ssh-bf' (6 events over 2.920062ms) at 2020-11-28 10:20:46.845619968 +0100 CET m=+5.903899761",
"scenario":"crowdsecurity/ssh-bf",
"scenario_hash":"4441dcff07020f6690d998b7101e642359ba405c2abb83565bbbdcee36de280f",
"scenario_version":"0.1",
"simulated":false,
"source":{
"as_name":"Cloudflare Inc",
"cn":"AU",
"ip":"1.1.1.172",
"latitude":-37.7,
"longitude":145.1833,
"range":"1.1.1.0/24",
"scope":"Ip",
"value":"1.1.1.172"
},
"start_at":"2020-11-28 10:20:46.842701127 +0100 +0100",
"stop_at":"2020-11-28 10:20:46.845621385 +0100 +0100"
}
]`)
})
tcapacity := int32(5)
tduration := "59m49.264032632s"
torigin := "crowdsec"
tscenario := "crowdsecurity/ssh-bf"
tscope := "Ip"
ttype := "ban"
tvalue := "1.1.1.172"
ttimestamp := "2020-11-28 10:20:46 +0000 UTC"
teventscount := int32(6)
tleakspeed := "10s"
tmessage := "Ip 1.1.1.172 performed 'crowdsecurity/ssh-bf' (6 events over 2.920062ms) at 2020-11-28 10:20:46.845619968 +0100 CET m=+5.903899761"
tscenariohash := "4441dcff07020f6690d998b7101e642359ba405c2abb83565bbbdcee36de280f"
tscenarioversion := "0.1"
tstartat := "2020-11-28 10:20:46.842701127 +0100 +0100"
tstopat := "2020-11-28 10:20:46.845621385 +0100 +0100"
expected := models.GetAlertsResponse{
&models.Alert{
Capacity: &tcapacity,
CreatedAt: "2020-11-28T10:20:47+01:00",
Decisions: []*models.Decision{
&models.Decision{
Duration: &tduration,
EndIP: 16843180,
ID: 1,
Origin: &torigin,
Scenario: &tscenario,
Scope: &tscope,
Simulated: new(bool), //false,
StartIP: 16843180,
Type: &ttype,
Value: &tvalue,
},
},
Events: []*models.Event{
&models.Event{
Meta: models.Meta{
&models.MetaItems0{
Key: "target_user",
Value: "netflix",
},
&models.MetaItems0{
Key: "service",
Value: "ssh",
},
},
Timestamp: &ttimestamp,
},
&models.Event{
Meta: models.Meta{
&models.MetaItems0{
Key: "target_user",
Value: "netflix",
},
&models.MetaItems0{
Key: "service",
Value: "ssh",
},
},
Timestamp: &ttimestamp,
},
},
EventsCount: &teventscount,
ID: 1,
Leakspeed: &tleakspeed,
MachineID: "test",
Message: &tmessage,
Remediation: false,
Scenario: &tscenario,
ScenarioHash: &tscenariohash,
ScenarioVersion: &tscenarioversion,
Simulated: new(bool), //(false),
Source: &models.Source{
AsName: "Cloudflare Inc",
AsNumber: "",
Cn: "AU",
IP: "1.1.1.172",
Latitude: -37.7,
Longitude: 145.1833,
Range: "1.1.1.0/24",
Scope: &tscope,
Value: &tvalue,
},
StartAt: &tstartat,
StopAt: &tstopat,
},
}
//log.Debugf("data : -> %s", spew.Sdump(alerts))
//log.Debugf("resp : -> %s", spew.Sdump(resp))
//log.Debugf("expected : -> %s", spew.Sdump(expected))
//first one returns data
alerts, resp, err := client.Alerts.List(context.Background(), AlertsListOpts{})
if err != nil {
log.Errorf("test Unable to list alerts : %+v", err)
}
if resp.Response.StatusCode != http.StatusOK {
t.Errorf("Alerts.List returned status: %d, want %d", resp.Response.StatusCode, http.StatusOK)
}
if !reflect.DeepEqual(*alerts, expected) {
t.Errorf("client.Alerts.List returned %+v, want %+v", resp, expected)
}
//this one doesn't
filter := AlertsListOpts{IPEquals: new(string)}
*filter.IPEquals = "1.2.3.4"
alerts, resp, err = client.Alerts.List(context.Background(), filter)
if err != nil {
log.Errorf("test Unable to list alerts : %+v", err)
}
if resp.Response.StatusCode != http.StatusOK {
t.Errorf("Alerts.List returned status: %d, want %d", resp.Response.StatusCode, http.StatusOK)
}
assert.Equal(t, 0, len(*alerts))
}
func TestAlertsGetAsMachine(t *testing.T) {
log.SetLevel(log.DebugLevel)
mux, urlx, teardown := setup()
mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"code": 200, "expire": "2030-01-02T15:04:05Z", "token": "oklol"}`))
})
log.Printf("URL is %s", urlx)
apiURL, err := url.Parse(urlx + "/")
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
client, err := NewClient(&Config{
MachineID: "test_login",
Password: "test_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
})
if err != nil {
log.Fatalf("new api client: %s", err.Error())
}
defer teardown()
mux.HandleFunc("/alerts/2", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, `{"message":"object not found"}`)
})
mux.HandleFunc("/alerts/1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"capacity":5,"created_at":"2020-11-28T10:20:47+01:00",
"decisions":[
{"duration":"59m49.264032632s",
"end_ip":16843180,
"id":1,
"origin":"crowdsec",
"scenario":"crowdsecurity/ssh-bf",
"scope":"Ip",
"simulated":false,
"start_ip":16843180,
"type":"ban",
"value":"1.1.1.172"}
],
"events":[
{"meta":[
{"key":"target_user","value":"netflix"},
{"key":"service","value":"ssh"}
],
"timestamp":"2020-11-28 10:20:46 +0000 UTC"},
{"meta":[
{"key":"target_user","value":"netflix"},
{"key":"service","value":"ssh"}
],
"timestamp":"2020-11-28 10:20:46 +0000 UTC"}
],
"events_count":6,
"id":1,
"labels":null,
"leakspeed":"10s",
"machine_id":"test",
"message":"Ip 1.1.1.172 performed 'crowdsecurity/ssh-bf' (6 events over 2.920062ms) at 2020-11-28 10:20:46.845619968 +0100 CET m=+5.903899761",
"scenario":"crowdsecurity/ssh-bf",
"scenario_hash":"4441dcff07020f6690d998b7101e642359ba405c2abb83565bbbdcee36de280f",
"scenario_version":"0.1",
"simulated":false,
"source":{
"as_name":"Cloudflare Inc",
"cn":"AU",
"ip":"1.1.1.172",
"latitude":-37.7,
"longitude":145.1833,
"range":"1.1.1.0/24",
"scope":"Ip",
"value":"1.1.1.172"
},
"start_at":"2020-11-28 10:20:46.842701127 +0100 +0100",
"stop_at":"2020-11-28 10:20:46.845621385 +0100 +0100"
}`)
})
tcapacity := int32(5)
tduration := "59m49.264032632s"
torigin := "crowdsec"
tscenario := "crowdsecurity/ssh-bf"
tscope := "Ip"
ttype := "ban"
tvalue := "1.1.1.172"
ttimestamp := "2020-11-28 10:20:46 +0000 UTC"
teventscount := int32(6)
tleakspeed := "10s"
tmessage := "Ip 1.1.1.172 performed 'crowdsecurity/ssh-bf' (6 events over 2.920062ms) at 2020-11-28 10:20:46.845619968 +0100 CET m=+5.903899761"
tscenariohash := "4441dcff07020f6690d998b7101e642359ba405c2abb83565bbbdcee36de280f"
tscenarioversion := "0.1"
tstartat := "2020-11-28 10:20:46.842701127 +0100 +0100"
tstopat := "2020-11-28 10:20:46.845621385 +0100 +0100"
expected := &models.Alert{
Capacity: &tcapacity,
CreatedAt: "2020-11-28T10:20:47+01:00",
Decisions: []*models.Decision{
&models.Decision{
Duration: &tduration,
EndIP: 16843180,
ID: 1,
Origin: &torigin,
Scenario: &tscenario,
Scope: &tscope,
Simulated: new(bool), //false,
StartIP: 16843180,
Type: &ttype,
Value: &tvalue,
},
},
Events: []*models.Event{
&models.Event{
Meta: models.Meta{
&models.MetaItems0{
Key: "target_user",
Value: "netflix",
},
&models.MetaItems0{
Key: "service",
Value: "ssh",
},
},
Timestamp: &ttimestamp,
},
&models.Event{
Meta: models.Meta{
&models.MetaItems0{
Key: "target_user",
Value: "netflix",
},
&models.MetaItems0{
Key: "service",
Value: "ssh",
},
},
Timestamp: &ttimestamp,
},
},
EventsCount: &teventscount,
ID: 1,
Leakspeed: &tleakspeed,
MachineID: "test",
Message: &tmessage,
Remediation: false,
Scenario: &tscenario,
ScenarioHash: &tscenariohash,
ScenarioVersion: &tscenarioversion,
Simulated: new(bool), //(false),
Source: &models.Source{
AsName: "Cloudflare Inc",
AsNumber: "",
Cn: "AU",
IP: "1.1.1.172",
Latitude: -37.7,
Longitude: 145.1833,
Range: "1.1.1.0/24",
Scope: &tscope,
Value: &tvalue,
},
StartAt: &tstartat,
StopAt: &tstopat,
}
alerts, resp, err := client.Alerts.GetByID(context.Background(), 1)
if resp.Response.StatusCode != http.StatusOK {
t.Errorf("Alerts.List returned status: %d, want %d", resp.Response.StatusCode, http.StatusOK)
}
if !reflect.DeepEqual(*alerts, *expected) {
t.Errorf("client.Alerts.List returned %+v, want %+v", resp, expected)
}
//fail
alerts, resp, err = client.Alerts.GetByID(context.Background(), 2)
assert.Contains(t, fmt.Sprintf("%s", err), "API error: object not found")
}
func TestAlertsCreateAsMachine(t *testing.T) {
log.SetLevel(log.DebugLevel)
mux, urlx, teardown := setup()
mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"code": 200, "expire": "2030-01-02T15:04:05Z", "token": "oklol"}`))
})
mux.HandleFunc("/alerts", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`["3"]`))
})
log.Printf("URL is %s", urlx)
apiURL, err := url.Parse(urlx + "/")
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
client, err := NewClient(&Config{
MachineID: "test_login",
Password: "test_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
})
if err != nil {
log.Fatalf("new api client: %s", err.Error())
}
defer teardown()
alert := models.AddAlertsRequest{}
alerts, resp, err := client.Alerts.Add(context.Background(), alert)
expected := &models.AddAlertsResponse{"3"}
if resp.Response.StatusCode != http.StatusOK {
t.Errorf("Alerts.List returned status: %d, want %d", resp.Response.StatusCode, http.StatusOK)
}
if !reflect.DeepEqual(*alerts, *expected) {
t.Errorf("client.Alerts.List returned %+v, want %+v", resp, expected)
}
}
func TestAlertsDeleteAsMachine(t *testing.T) {
log.SetLevel(log.DebugLevel)
mux, urlx, teardown := setup()
mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"code": 200, "expire": "2030-01-02T15:04:05Z", "token": "oklol"}`))
})
mux.HandleFunc("/alerts", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
assert.Equal(t, r.URL.RawQuery, "ip=1.2.3.4")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message":"0 deleted alerts"}`))
})
log.Printf("URL is %s", urlx)
apiURL, err := url.Parse(urlx + "/")
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
client, err := NewClient(&Config{
MachineID: "test_login",
Password: "test_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
})
if err != nil {
log.Fatalf("new api client: %s", err.Error())
}
defer teardown()
alert := AlertsDeleteOpts{IPEquals: new(string)}
*alert.IPEquals = "1.2.3.4"
alerts, resp, err := client.Alerts.Delete(context.Background(), alert)
expected := &models.DeleteAlertsResponse{""}
if resp.Response.StatusCode != http.StatusOK {
t.Errorf("Alerts.List returned status: %d, want %d", resp.Response.StatusCode, http.StatusOK)
}
if !reflect.DeepEqual(*alerts, *expected) {
t.Errorf("client.Alerts.List returned %+v, want %+v", resp, expected)
}
}

View file

@ -32,7 +32,7 @@ type APIKeyTransport struct {
// RoundTrip implements the RoundTripper interface. // RoundTrip implements the RoundTripper interface.
func (t *APIKeyTransport) RoundTrip(req *http.Request) (*http.Response, error) { func (t *APIKeyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if t.APIKey == "" { if t.APIKey == "" {
return nil, errors.New("t.APIKey is empty") return nil, errors.New("APIKey is empty")
} }
// We must make a copy of the Request so // We must make a copy of the Request so
@ -97,7 +97,7 @@ func (t *JWTTransport) refreshJwtToken() error {
if err != nil { if err != nil {
return fmt.Errorf("can't update scenario list: %s", err) return fmt.Errorf("can't update scenario list: %s", err)
} }
log.Infof("scenarios liste updated for '%s'", *t.MachineID) log.Infof("scenarios list updated for '%s'", *t.MachineID)
} }
var auth = models.WatcherAuthRequest{ var auth = models.WatcherAuthRequest{

View file

@ -0,0 +1,181 @@
package apiclient
import (
"bytes"
"context"
"fmt"
"net/http"
"net/url"
"testing"
"github.com/crowdsecurity/crowdsec/pkg/cwversion"
"github.com/crowdsecurity/crowdsec/pkg/models"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func TestWatcherAuth(t *testing.T) {
log.SetLevel(log.DebugLevel)
mux, urlx, teardown := setup()
defer teardown()
//body: models.WatcherRegistrationRequest{MachineID: &config.MachineID, Password: &config.Password}
mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
buf := new(bytes.Buffer)
_, _ = buf.ReadFrom(r.Body)
newStr := buf.String()
log.Printf("--> %s", newStr)
if newStr == `{"machine_id":"test_login","password":"test_password","scenarios":["crowdsecurity/test"]}
` {
log.Printf("ok cool")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"code":200,"expire":"2029-11-30T14:14:24+01:00","token":"toto"}`)
} else {
w.WriteHeader(http.StatusForbidden)
log.Printf("badbad")
fmt.Fprintf(w, `{"message":"access forbidden"}`)
}
})
log.Printf("URL is %s", urlx)
apiURL, err := url.Parse(urlx + "/")
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
//ok auth
mycfg := &Config{
MachineID: "test_login",
Password: "test_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
Scenarios: []string{"crowdsecurity/test"},
}
client, err := NewClient(mycfg)
if err != nil {
log.Fatalf("new api client: %s", err.Error())
}
_, err = client.Auth.AuthenticateWatcher(context.Background(), models.WatcherAuthRequest{
MachineID: &mycfg.MachineID,
Password: &mycfg.Password,
Scenarios: mycfg.Scenarios,
})
if err != nil {
t.Fatalf("unexpect auth err 0: %s", err)
}
//bad auth
mycfg = &Config{
MachineID: "BADtest_login",
Password: "BADtest_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
Scenarios: []string{"crowdsecurity/test"},
}
client, err = NewClient(mycfg)
if err != nil {
log.Fatalf("new api client: %s", err.Error())
}
_, err = client.Auth.AuthenticateWatcher(context.Background(), models.WatcherAuthRequest{
MachineID: &mycfg.MachineID,
Password: &mycfg.Password,
})
assert.Contains(t, err.Error(), "403 Forbidden")
}
func TestWatcherRegister(t *testing.T) {
log.SetLevel(log.DebugLevel)
mux, urlx, teardown := setup()
defer teardown()
//body: models.WatcherRegistrationRequest{MachineID: &config.MachineID, Password: &config.Password}
mux.HandleFunc("/watchers", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
buf := new(bytes.Buffer)
_, _ = buf.ReadFrom(r.Body)
newStr := buf.String()
assert.Equal(t, newStr, `{"machine_id":"test_login","password":"test_password"}
`)
w.WriteHeader(http.StatusOK)
})
log.Printf("URL is %s", urlx)
apiURL, err := url.Parse(urlx + "/")
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
client, err := RegisterClient(&Config{
MachineID: "test_login",
Password: "test_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
}, &http.Client{})
if err != nil {
t.Fatalf("while registering client : %s", err)
}
log.Printf("->%T", client)
}
func TestWatcherUnregister(t *testing.T) {
log.SetLevel(log.DebugLevel)
mux, urlx, teardown := setup()
defer teardown()
//body: models.WatcherRegistrationRequest{MachineID: &config.MachineID, Password: &config.Password}
mux.HandleFunc("/watchers", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
assert.Equal(t, r.ContentLength, int64(0))
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
buf := new(bytes.Buffer)
_, _ = buf.ReadFrom(r.Body)
newStr := buf.String()
if newStr == `{"machine_id":"test_login","password":"test_password","scenarios":["crowdsecurity/test"]}
` {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"code":200,"expire":"2029-11-30T14:14:24+01:00","token":"toto"}`)
} else {
w.WriteHeader(http.StatusForbidden)
fmt.Fprintf(w, `{"message":"access forbidden"}`)
}
})
log.Printf("URL is %s", urlx)
apiURL, err := url.Parse(urlx + "/")
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
mycfg := &Config{
MachineID: "test_login",
Password: "test_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
Scenarios: []string{"crowdsecurity/test"},
}
client, err := NewClient(mycfg)
if err != nil {
log.Fatalf("new api client: %s", err.Error())
}
_, err = client.Auth.UnregisterWatcher(context.Background())
if err != nil {
t.Fatalf("while registering client : %s", err)
}
log.Printf("->%T", client)
}

View file

@ -0,0 +1,83 @@
package apiclient
import (
"context"
"net/http"
"net/url"
"testing"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func TestApiAuth(t *testing.T) {
log.SetLevel(log.TraceLevel)
mux, urlx, teardown := setup()
mux.HandleFunc("/decisions", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
if r.Header.Get("X-Api-Key") == "ixu" {
assert.Equal(t, r.URL.RawQuery, "ip=1.2.3.4")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`null`))
} else {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte(`{"message":"access forbidden"}`))
}
})
log.Printf("URL is %s", urlx)
apiURL, err := url.Parse(urlx + "/")
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
defer teardown()
//ok no answer
auth := &APIKeyTransport{
APIKey: "ixu",
}
newcli, err := NewDefaultClient(apiURL, "v1", "toto", auth.Client())
if err != nil {
log.Fatalf("new api client: %s", err.Error())
}
alert := DecisionsListOpts{IPEquals: new(string)}
*alert.IPEquals = "1.2.3.4"
_, resp, err := newcli.Decisions.List(context.Background(), alert)
if resp.Response.StatusCode != http.StatusOK {
t.Errorf("Alerts.List returned status: %d, want %d", resp.Response.StatusCode, http.StatusOK)
}
//ko bad token
auth = &APIKeyTransport{
APIKey: "bad",
}
newcli, err = NewDefaultClient(apiURL, "v1", "toto", auth.Client())
if err != nil {
log.Fatalf("new api client: %s", err.Error())
}
_, resp, err = newcli.Decisions.List(context.Background(), alert)
log.Infof("--> %s", err)
if resp.Response.StatusCode != http.StatusForbidden {
t.Errorf("Alerts.List returned status: %d, want %d", resp.Response.StatusCode, http.StatusOK)
}
assert.Contains(t, err.Error(), "API error: access forbidden")
//ko empty token
auth = &APIKeyTransport{}
newcli, err = NewDefaultClient(apiURL, "v1", "toto", auth.Client())
if err != nil {
log.Fatalf("new api client: %s", err.Error())
}
_, resp, err = newcli.Decisions.List(context.Background(), alert)
log.Infof("--> %s", err)
assert.Contains(t, err.Error(), "APIKey is empty")
}

View file

@ -86,11 +86,14 @@ func RegisterClient(config *Config, client *http.Client) (*ApiClient, error) {
c.Alerts = (*AlertsService)(&c.common) c.Alerts = (*AlertsService)(&c.common)
c.Auth = (*AuthService)(&c.common) c.Auth = (*AuthService)(&c.common)
_, err := c.Auth.RegisterWatcher(context.Background(), models.WatcherRegistrationRequest{MachineID: &config.MachineID, Password: &config.Password}) resp, err := c.Auth.RegisterWatcher(context.Background(), models.WatcherRegistrationRequest{MachineID: &config.MachineID, Password: &config.Password})
/*if we have http status, return it*/
if err != nil { if err != nil {
return c, errors.Wrapf(err, "api register (%s): %s", c.BaseURL, err) if resp != nil && resp.Response != nil {
return nil, errors.Wrapf(err, "api register (%s) http %s : %s", c.BaseURL, resp.Response.Status, err)
}
return nil, errors.Wrapf(err, "api register (%s) : %s", c.BaseURL, err)
} }
return c, nil return c, nil
} }
@ -107,7 +110,11 @@ type ErrorResponse struct {
} }
func (e *ErrorResponse) Error() string { func (e *ErrorResponse) Error() string {
return fmt.Sprintf("API error (%s) : %s", *e.Message, e.Errors) err := fmt.Sprintf("API error: %s", *e.Message)
if len(e.Errors) > 0 {
err += fmt.Sprintf(" (%s)", e.Errors)
}
return err
} }
func newResponse(r *http.Response) *Response { func newResponse(r *http.Response) *Response {
@ -123,7 +130,13 @@ func CheckResponse(r *http.Response) error {
errorResponse := &ErrorResponse{} errorResponse := &ErrorResponse{}
data, err := ioutil.ReadAll(r.Body) data, err := ioutil.ReadAll(r.Body)
if err == nil && data != nil { if err == nil && data != nil {
json.Unmarshal(data, errorResponse) err := json.Unmarshal(data, errorResponse)
if err != nil {
return errors.Wrapf(err, "http code %d, invalid body", r.StatusCode)
}
} else {
errorResponse.Message = new(string)
*errorResponse.Message = fmt.Sprintf("http code %d, no error message", r.StatusCode)
} }
return errorResponse return errorResponse
} }

View file

@ -71,11 +71,12 @@ func (c *ApiClient) Do(ctx context.Context, req *http.Request, v interface{}) (*
if e, ok := err.(*url.Error); ok { if e, ok := err.(*url.Error); ok {
if url, err := url.Parse(e.URL); err == nil { if url, err := url.Parse(e.URL); err == nil {
e.URL = url.String() e.URL = url.String()
return nil, e return newResponse(resp), e
} else {
return newResponse(resp), err
} }
} }
return newResponse(resp), err
return nil, err
} }
response := newResponse(resp) response := newResponse(resp)
@ -98,6 +99,5 @@ func (c *ApiClient) Do(ctx context.Context, req *http.Request, v interface{}) (*
} }
} }
} }
return response, err return response, err
} }

View file

@ -0,0 +1,77 @@
package apiclient
import (
"context"
"fmt"
"net/http"
"net/url"
"testing"
"time"
"github.com/crowdsecurity/crowdsec/pkg/cwversion"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func TestNewRequestInvalid(t *testing.T) {
mux, urlx, teardown := setup()
defer teardown()
//missing slash in uri
apiURL, err := url.Parse(urlx)
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
client, err := NewClient(&Config{
MachineID: "test_login",
Password: "test_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
})
if err != nil {
t.Fatalf("new api client: %s", err.Error())
}
/*mock login*/
mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"code": 401, "message" : "bad login/password"}`))
})
mux.HandleFunc("/alerts", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
w.WriteHeader(http.StatusOK)
})
_, _, err = client.Alerts.List(context.Background(), AlertsListOpts{})
assert.Contains(t, err.Error(), `building request: BaseURL must have a trailing slash, but `)
}
func TestNewRequestTimeout(t *testing.T) {
mux, urlx, teardown := setup()
defer teardown()
//missing slash in uri
apiURL, err := url.Parse(urlx + "/")
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
client, err := NewClient(&Config{
MachineID: "test_login",
Password: "test_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
})
if err != nil {
t.Fatalf("new api client: %s", err.Error())
}
/*mock login*/
mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
})
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
_, _, err = client.Alerts.List(ctx, AlertsListOpts{})
assert.Contains(t, err.Error(), `performing request: context deadline exceeded`)
}

View file

@ -0,0 +1,200 @@
package apiclient
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/crowdsecurity/crowdsec/pkg/cwversion"
log "github.com/sirupsen/logrus"
)
/*this is a ripoff of google/go-github approach :
- setup a test http server along with a client that is configured to talk to test server
- each test will then bind handler for the method(s) they want to try
*/
func setup() (mux *http.ServeMux, serverURL string, teardown func()) {
// mux is the HTTP request multiplexer used with the test server.
mux = http.NewServeMux()
baseURLPath := "/v1"
apiHandler := http.NewServeMux()
apiHandler.Handle(baseURLPath+"/", http.StripPrefix(baseURLPath, mux))
// server is a test HTTP server used to provide mock API responses.
server := httptest.NewServer(apiHandler)
return mux, server.URL, server.Close
}
func testMethod(t *testing.T, r *http.Request, want string) {
t.Helper()
if got := r.Method; got != want {
t.Errorf("Request method: %v, want %v", got, want)
}
}
func TestNewClientOk(t *testing.T) {
mux, urlx, teardown := setup()
defer teardown()
apiURL, err := url.Parse(urlx + "/")
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
client, err := NewClient(&Config{
MachineID: "test_login",
Password: "test_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
})
if err != nil {
t.Fatalf("new api client: %s", err.Error())
}
/*mock login*/
mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"code": 200, "expire": "2030-01-02T15:04:05Z", "token": "oklol"}`))
})
mux.HandleFunc("/alerts", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
w.WriteHeader(http.StatusOK)
})
_, resp, err := client.Alerts.List(context.Background(), AlertsListOpts{})
if err != nil {
t.Fatalf("test Unable to list alerts : %+v", err)
}
if resp.Response.StatusCode != http.StatusOK {
t.Fatalf("Alerts.List returned status: %d, want %d", resp.Response.StatusCode, http.StatusCreated)
}
}
func TestNewClientKo(t *testing.T) {
mux, urlx, teardown := setup()
defer teardown()
apiURL, err := url.Parse(urlx + "/")
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
client, err := NewClient(&Config{
MachineID: "test_login",
Password: "test_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
})
if err != nil {
t.Fatalf("new api client: %s", err.Error())
}
/*mock login*/
mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"code": 401, "message" : "bad login/password"}`))
})
mux.HandleFunc("/alerts", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
w.WriteHeader(http.StatusOK)
})
_, _, err = client.Alerts.List(context.Background(), AlertsListOpts{})
assert.Contains(t, err.Error(), `received response status "401 Unauthorized"`)
log.Printf("err-> %s", err)
}
func TestNewDefaultClient(t *testing.T) {
mux, urlx, teardown := setup()
defer teardown()
apiURL, err := url.Parse(urlx + "/")
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
client, err := NewDefaultClient(apiURL, "/v1", "", nil)
if err != nil {
t.Fatalf("new api client: %s", err.Error())
}
mux.HandleFunc("/alerts", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"code": 401, "message" : "brr"}`))
})
_, _, err = client.Alerts.List(context.Background(), AlertsListOpts{})
assert.Contains(t, err.Error(), `performing request: API error: brr`)
log.Printf("err-> %s", err)
}
func TestNewClientRegisterKO(t *testing.T) {
apiURL, err := url.Parse("http://127.0.0.1:4242/")
if err != nil {
t.Fatalf("parsing api url: %s", apiURL)
}
_, err = RegisterClient(&Config{
MachineID: "test_login",
Password: "test_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
}, &http.Client{})
assert.Contains(t, fmt.Sprintf("%s", err), "dial tcp 127.0.0.1:4242: connect: connection refused")
}
func TestNewClientRegisterOK(t *testing.T) {
log.SetLevel(log.TraceLevel)
mux, urlx, teardown := setup()
defer teardown()
/*mock login*/
mux.HandleFunc("/watchers", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"code": 200, "expire": "2030-01-02T15:04:05Z", "token": "oklol"}`))
})
apiURL, err := url.Parse(urlx + "/")
if err != nil {
t.Fatalf("parsing api url: %s", apiURL)
}
client, err := RegisterClient(&Config{
MachineID: "test_login",
Password: "test_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
}, &http.Client{})
if err != nil {
t.Fatalf("while registering client : %s", err)
}
log.Printf("->%T", client)
}
func TestNewClientBadAnswer(t *testing.T) {
log.SetLevel(log.TraceLevel)
mux, urlx, teardown := setup()
defer teardown()
/*mock login*/
mux.HandleFunc("/watchers", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`bad`))
})
apiURL, err := url.Parse(urlx + "/")
if err != nil {
t.Fatalf("parsing api url: %s", apiURL)
}
_, err = RegisterClient(&Config{
MachineID: "test_login",
Password: "test_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
}, &http.Client{})
assert.Contains(t, fmt.Sprintf("%s", err), `invalid body: invalid character 'b' looking for beginning of value`)
}

View file

@ -35,7 +35,7 @@ func (s *DecisionsService) List(ctx context.Context, opts DecisionsListOpts) (*m
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
u := fmt.Sprintf("%s/decisions/?%s", s.client.URLPrefix, params.Encode()) u := fmt.Sprintf("%s/decisions?%s", s.client.URLPrefix, params.Encode())
req, err := s.client.NewRequest("GET", u, nil) req, err := s.client.NewRequest("GET", u, nil)
if err != nil { if err != nil {

View file

@ -0,0 +1,273 @@
package apiclient
import (
"context"
"fmt"
"net/http"
"net/url"
"reflect"
"testing"
"github.com/crowdsecurity/crowdsec/pkg/cwversion"
"github.com/crowdsecurity/crowdsec/pkg/models"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func TestDecisionsList(t *testing.T) {
log.SetLevel(log.DebugLevel)
mux, urlx, teardown := setup()
defer teardown()
mux.HandleFunc("/decisions", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
if r.URL.RawQuery == "ip=1.2.3.4" {
assert.Equal(t, r.URL.RawQuery, "ip=1.2.3.4")
assert.Equal(t, r.Header.Get("X-Api-Key"), "ixu")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`[{"duration":"3h59m55.756182786s","end_ip":16909060,"id":4,"origin":"cscli","scenario":"manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'","scope":"Ip","start_ip":16909060,"type":"ban","value":"1.2.3.4"}]`))
} else {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`null`))
//no results
}
})
apiURL, err := url.Parse(urlx + "/")
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
//ok answer
auth := &APIKeyTransport{
APIKey: "ixu",
}
newcli, err := NewDefaultClient(apiURL, "v1", "toto", auth.Client())
if err != nil {
log.Fatalf("new api client: %s", err.Error())
}
tduration := "3h59m55.756182786s"
torigin := "cscli"
tscenario := "manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'"
tscope := "Ip"
ttype := "ban"
tvalue := "1.2.3.4"
expected := &models.GetDecisionsResponse{
&models.Decision{
Duration: &tduration,
EndIP: 16909060,
ID: 4,
Origin: &torigin,
Scenario: &tscenario,
Scope: &tscope,
StartIP: 16909060,
Type: &ttype,
Value: &tvalue,
},
}
//OK decisions
decisionsFilter := DecisionsListOpts{IPEquals: new(string)}
*decisionsFilter.IPEquals = "1.2.3.4"
decisions, resp, err := newcli.Decisions.List(context.Background(), decisionsFilter)
if resp.Response.StatusCode != http.StatusOK {
t.Errorf("Alerts.List returned status: %d, want %d", resp.Response.StatusCode, http.StatusOK)
}
if err != nil {
log.Fatalf("new api client: %s", err.Error())
}
if !reflect.DeepEqual(*decisions, *expected) {
t.Fatalf("returned %+v, want %+v", resp, expected)
}
//Empty return
decisionsFilter = DecisionsListOpts{IPEquals: new(string)}
*decisionsFilter.IPEquals = "1.2.3.5"
decisions, resp, err = newcli.Decisions.List(context.Background(), decisionsFilter)
if resp.Response.StatusCode != http.StatusOK {
t.Errorf("Alerts.List returned status: %d, want %d", resp.Response.StatusCode, http.StatusOK)
}
assert.Equal(t, len(*decisions), 0)
}
func TestDecisionsStream(t *testing.T) {
log.SetLevel(log.DebugLevel)
mux, urlx, teardown := setup()
defer teardown()
mux.HandleFunc("/decisions/stream", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.Header.Get("X-Api-Key"), "ixu")
testMethod(t, r, "GET")
if r.Method == "GET" {
if r.URL.RawQuery == "startup=true" {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"deleted":null,"new":[{"duration":"3h59m55.756182786s","end_ip":16909060,"id":4,"origin":"cscli","scenario":"manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'","scope":"Ip","start_ip":16909060,"type":"ban","value":"1.2.3.4"}]}`))
} else {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"deleted":null,"new":null}`))
}
}
})
mux.HandleFunc("/decisions", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.Header.Get("X-Api-Key"), "ixu")
testMethod(t, r, "DELETE")
if r.Method == "DELETE" {
w.WriteHeader(http.StatusOK)
}
})
apiURL, err := url.Parse(urlx + "/")
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
//ok answer
auth := &APIKeyTransport{
APIKey: "ixu",
}
newcli, err := NewDefaultClient(apiURL, "v1", "toto", auth.Client())
if err != nil {
log.Fatalf("new api client: %s", err.Error())
}
tduration := "3h59m55.756182786s"
torigin := "cscli"
tscenario := "manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'"
tscope := "Ip"
ttype := "ban"
tvalue := "1.2.3.4"
expected := &models.DecisionsStreamResponse{
New: models.GetDecisionsResponse{
&models.Decision{
Duration: &tduration,
EndIP: 16909060,
ID: 4,
Origin: &torigin,
Scenario: &tscenario,
Scope: &tscope,
StartIP: 16909060,
Type: &ttype,
Value: &tvalue,
},
},
}
decisions, resp, err := newcli.Decisions.GetStream(context.Background(), true)
if resp.Response.StatusCode != http.StatusOK {
t.Errorf("Alerts.List returned status: %d, want %d", resp.Response.StatusCode, http.StatusOK)
}
if err != nil {
log.Fatalf("new api client: %s", err.Error())
}
if !reflect.DeepEqual(*decisions, *expected) {
t.Fatalf("returned %+v, want %+v", resp, expected)
}
//and second call, we get empty lists
decisions, resp, err = newcli.Decisions.GetStream(context.Background(), false)
if resp.Response.StatusCode != http.StatusOK {
t.Errorf("Alerts.List returned status: %d, want %d", resp.Response.StatusCode, http.StatusOK)
}
assert.Equal(t, 0, len(decisions.New))
assert.Equal(t, 0, len(decisions.Deleted))
//delete stream
resp, err = newcli.Decisions.StopStream(context.Background())
if resp.Response.StatusCode != http.StatusOK {
t.Errorf("Alerts.List returned status: %d, want %d", resp.Response.StatusCode, http.StatusOK)
}
}
func TestDeleteDecisions(t *testing.T) {
mux, urlx, teardown := setup()
mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"code": 200, "expire": "2030-01-02T15:04:05Z", "token": "oklol"}`))
})
mux.HandleFunc("/decisions", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
assert.Equal(t, r.URL.RawQuery, "ip=1.2.3.4")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"nbDeleted":"1"}`))
//w.Write([]byte(`{"message":"0 deleted alerts"}`))
})
log.Printf("URL is %s", urlx)
apiURL, err := url.Parse(urlx + "/")
if err != nil {
log.Fatalf("parsing api url: %s", apiURL)
}
client, err := NewClient(&Config{
MachineID: "test_login",
Password: "test_password",
UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
URL: apiURL,
VersionPrefix: "v1",
})
if err != nil {
log.Fatalf("new api client: %s", err.Error())
}
filters := DecisionsDeleteOpts{IPEquals: new(string)}
*filters.IPEquals = "1.2.3.4"
deleted, _, err := client.Decisions.Delete(context.Background(), filters)
if err != nil {
t.Fatalf("unexpected err : %s", err)
}
assert.Equal(t, "1", deleted.NbDeleted)
defer teardown()
}
// func TestDeleteOneDecision(t *testing.T) {
// mux, urlx, teardown := setup()
// mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) {
// w.WriteHeader(http.StatusOK)
// w.Write([]byte(`{"code": 200, "expire": "2030-01-02T15:04:05Z", "token": "oklol"}`))
// })
// mux.HandleFunc("/decisions/1", func(w http.ResponseWriter, r *http.Request) {
// testMethod(t, r, "DELETE")
// w.WriteHeader(http.StatusOK)
// w.Write([]byte(`{"nbDeleted":"1"}`))
// })
// log.Printf("URL is %s", urlx)
// apiURL, err := url.Parse(urlx + "/")
// if err != nil {
// log.Fatalf("parsing api url: %s", apiURL)
// }
// client, err := NewClient(&Config{
// MachineID: "test_login",
// Password: "test_password",
// UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
// URL: apiURL,
// VersionPrefix: "v1",
// })
// if err != nil {
// log.Fatalf("new api client: %s", err.Error())
// }
// filters := DecisionsDeleteOpts{IPEquals: new(string)}
// *filters.IPEquals = "1.2.3.4"
// deleted, _, err := client.Decisions.Delete(context.Background(), filters)
// if err != nil {
// t.Fatalf("unexpected err : %s", err)
// }
// assert.Equal(t, "1", deleted.NbDeleted)
// defer teardown()
// }

View file

@ -130,7 +130,7 @@ func TestCreateAlert(t *testing.T) {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", loginResp.Token)) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", loginResp.Token))
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 201, w.Code)
assert.Equal(t, "[\"1\"]", w.Body.String()) assert.Equal(t, "[\"1\"]", w.Body.String())
} }
@ -538,5 +538,5 @@ func TestDeleteAlert(t *testing.T) {
req.RemoteAddr = "127.0.0.1:4242" req.RemoteAddr = "127.0.0.1:4242"
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, `{"message":"1 deleted alerts"}`, w.Body.String()) assert.Equal(t, `{"nbDeleted":"1"}`, w.Body.String())
} }

View file

@ -150,6 +150,5 @@ func TestUnknownPath(t *testing.T) {
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
assert.Equal(t, 404, w.Code) assert.Equal(t, 404, w.Code)
assert.Equal(t, "{\"message\":\"Page or Method not found\"}", w.Body.String())
} }

View file

@ -2,6 +2,7 @@ package controllers
import ( import (
"context" "context"
"net/http"
v1 "github.com/crowdsecurity/crowdsec/pkg/apiserver/controllers/v1" v1 "github.com/crowdsecurity/crowdsec/pkg/apiserver/controllers/v1"
"github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csconfig"
@ -43,6 +44,14 @@ func (c *Controller) NewV1() error {
} }
c.Router.Use(v1.PrometheusMiddleware()) c.Router.Use(v1.PrometheusMiddleware())
c.Router.HandleMethodNotAllowed = true
c.Router.NoRoute(func(ctx *gin.Context) {
ctx.AbortWithStatus(http.StatusNotFound)
})
c.Router.NoMethod(func(ctx *gin.Context) {
ctx.AbortWithStatus(http.StatusMethodNotAllowed)
})
groupV1 := c.Router.Group("/v1") groupV1 := c.Router.Group("/v1")
groupV1.POST("/watchers", handlerV1.CreateMachine) groupV1.POST("/watchers", handlerV1.CreateMachine)
groupV1.POST("/watchers/login", handlerV1.Middlewares.JWT.Middleware.LoginHandler) groupV1.POST("/watchers/login", handlerV1.Middlewares.JWT.Middleware.LoginHandler)

View file

@ -146,7 +146,7 @@ func (c *Controller) CreateAlert(gctx *gin.Context) {
log.Warningf("Cannot send alert to Central API channel") log.Warningf("Cannot send alert to Central API channel")
} }
gctx.JSON(http.StatusOK, alerts) gctx.JSON(http.StatusCreated, alerts)
return return
} }
@ -202,11 +202,15 @@ func (c *Controller) DeleteAlerts(gctx *gin.Context) {
return return
} }
var err error var err error
deleted, err := c.DBClient.DeleteAlertWithFilter(gctx.Request.URL.Query()) nbDeleted, err := c.DBClient.DeleteAlertWithFilter(gctx.Request.URL.Query())
if err != nil { if err != nil {
c.HandleDBErrors(gctx, err) c.HandleDBErrors(gctx, err)
} }
gctx.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("%d deleted alerts", len(deleted))}) deleteAlertsResp := models.DeleteAlertsResponse{
NbDeleted: strconv.Itoa(nbDeleted),
}
gctx.JSON(http.StatusOK, deleteAlertsResp)
return return
} }

View file

@ -10,6 +10,9 @@ import (
func (c *Controller) HandleDBErrors(gctx *gin.Context, err error) { func (c *Controller) HandleDBErrors(gctx *gin.Context, err error) {
switch errors.Cause(err) { switch errors.Cause(err) {
case database.ItemNotFound:
gctx.JSON(http.StatusNotFound, gin.H{"message": err.Error()})
return
case database.UserExists: case database.UserExists:
gctx.JSON(http.StatusForbidden, gin.H{"message": err.Error()}) gctx.JSON(http.StatusForbidden, gin.H{"message": err.Error()})
return return

View file

@ -28,6 +28,6 @@ func (c *Controller) CreateMachine(gctx *gin.Context) {
return return
} }
gctx.Status(http.StatusOK) gctx.Status(http.StatusCreated)
return return
} }

View file

@ -47,7 +47,7 @@ func TestCreateMachine(t *testing.T) {
req.Header.Add("User-Agent", UserAgent) req.Header.Add("User-Agent", UserAgent)
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 201, w.Code)
assert.Equal(t, "", w.Body.String()) assert.Equal(t, "", w.Body.String())
} }

View file

@ -491,7 +491,7 @@ func (c *Client) DeleteAlertGraph(alertItem *ent.Alert) error {
return nil return nil
} }
func (c *Client) DeleteAlertWithFilter(filter map[string][]string) ([]*ent.Alert, error) { func (c *Client) DeleteAlertWithFilter(filter map[string][]string) (int, error) {
var err error var err error
// Get all the alerts that match the filter // Get all the alerts that match the filter
@ -501,10 +501,10 @@ func (c *Client) DeleteAlertWithFilter(filter map[string][]string) ([]*ent.Alert
err = c.DeleteAlertGraph(alertItem) err = c.DeleteAlertGraph(alertItem)
if err != nil { if err != nil {
log.Warningf("DeleteAlertWithFilter : %s", err) log.Warningf("DeleteAlertWithFilter : %s", err)
return []*ent.Alert{}, errors.Wrapf(DeleteFail, "event with alert ID '%d'", alertItem.ID) return 0, errors.Wrapf(DeleteFail, "event with alert ID '%d'", alertItem.ID)
} }
} }
return alertsToDelete, nil return len(alertsToDelete), nil
} }
func (c *Client) FlushAlerts(MaxAge string, MaxItems int) error { func (c *Client) FlushAlerts(MaxAge string, MaxItems int) error {
@ -521,12 +521,12 @@ func (c *Client) FlushAlerts(MaxAge string, MaxItems int) error {
filter := map[string][]string{ filter := map[string][]string{
"created_before": {MaxAge}, "created_before": {MaxAge},
} }
deleted, err := c.DeleteAlertWithFilter(filter) nbDeleted, err := c.DeleteAlertWithFilter(filter)
if err != nil { if err != nil {
log.Warningf("FlushAlerts (max age) : %s", err) log.Warningf("FlushAlerts (max age) : %s", err)
return errors.Wrapf(err, "unable to flush alerts with filter until: %s", MaxAge) return errors.Wrapf(err, "unable to flush alerts with filter until: %s", MaxAge)
} }
deletedByAge = len(deleted) deletedByAge = nbDeleted
} }
if MaxItems > 0 { if MaxItems > 0 {
if totalAlerts > MaxItems { if totalAlerts > MaxItems {
@ -563,8 +563,13 @@ func (c *Client) FlushAlerts(MaxAge string, MaxItems int) error {
func (c *Client) GetAlertByID(alertID int) (*ent.Alert, error) { func (c *Client) GetAlertByID(alertID int) (*ent.Alert, error) {
alert, err := c.Ent.Alert.Query().Where(alert.IDEQ(alertID)).WithDecisions().WithEvents().WithMetas().WithOwner().First(c.CTX) alert, err := c.Ent.Alert.Query().Where(alert.IDEQ(alertID)).WithDecisions().WithEvents().WithMetas().WithOwner().First(c.CTX)
if err != nil { if err != nil {
/*record not found, 404*/
if ent.IsNotFound(err) {
log.Warningf("GetAlertByID (not found): %s", err)
return &ent.Alert{}, ItemNotFound
}
log.Warningf("GetAlertByID : %s", err) log.Warningf("GetAlertByID : %s", err)
return &ent.Alert{}, errors.Wrapf(QueryFail, "alert id '%d'", alertID) return &ent.Alert{}, QueryFail
} }
return alert, nil return alert, nil
} }

View file

@ -278,5 +278,9 @@ func (c *Client) SoftDeleteDecisionByID(decisionID int) error {
log.Warningf("SoftDeleteDecisionByID : %v (nb soft deleted: %d)", err, nbUpdated) log.Warningf("SoftDeleteDecisionByID : %v (nb soft deleted: %d)", err, nbUpdated)
return errors.Wrapf(DeleteFail, "decision with id '%d' doesn't exist", decisionID) return errors.Wrapf(DeleteFail, "decision with id '%d' doesn't exist", decisionID)
} }
if nbUpdated == 0 {
return ItemNotFound
}
return nil return nil
} }

View file

@ -10,6 +10,7 @@ var (
QueryFail = errors.New("unable to query") QueryFail = errors.New("unable to query")
UpdateFail = errors.New("unable to update") UpdateFail = errors.New("unable to update")
DeleteFail = errors.New("unable to delete") DeleteFail = errors.New("unable to delete")
ItemNotFound = errors.New("object not found")
ParseTimeFail = errors.New("unable to parse time") ParseTimeFail = errors.New("unable to parse time")
ParseDurationFail = errors.New("unable to parse duration") ParseDurationFail = errors.New("unable to parse duration")
MarshalFail = errors.New("unable to marshal") MarshalFail = errors.New("unable to marshal")

View file

@ -267,8 +267,8 @@ paths:
schema: schema:
$ref: '#/definitions/WatcherRegistrationRequest' $ref: '#/definitions/WatcherRegistrationRequest'
responses: responses:
'200': '201':
description: Watcher registered description: Watcher Created
headers: {} headers: {}
'400': '400':
description: "400 response" description: "400 response"
@ -322,8 +322,8 @@ paths:
schema: schema:
$ref: '#/definitions/AddAlertsRequest' $ref: '#/definitions/AddAlertsRequest'
responses: responses:
'200': '201':
description: successful operation description: Alert(s) created
schema: schema:
$ref: '#/definitions/AddAlertsResponse' $ref: '#/definitions/AddAlertsResponse'
headers: {} headers: {}