diff --git a/data/conf/samples/fraud_internal/cgrates.json b/data/conf/samples/fraud_internal/cgrates.json new file mode 100644 index 000000000..6e85ff7b3 --- /dev/null +++ b/data/conf/samples/fraud_internal/cgrates.json @@ -0,0 +1,80 @@ +{ + +"general": { + "log_level": 7, + "node_id": "CGR_FRAUD" +}, + +"listen": { + "rpc_json": ":2012", + "rpc_gob": ":2013", + "http": ":2080" +}, + +"data_db": { + "db_type": "*internal" +}, + +"stor_db": { + "db_type": "*internal" +}, + +"rals": { + "enabled": true +}, + +"schedulers": { + "enabled": true +}, + +"filters": { + "stats_conns": ["*internal"], + "apiers_conns": ["*internal"] +}, + +"stats": { + "enabled": true, + "indexed_selects":false, + "thresholds_conns": ["*internal"], + "store_interval": "-1" +}, + +"thresholds": { + "enabled": true, + "indexed_selects":false, + "store_interval": "-1" +}, + +"apiers": { + "enabled": true, + "caches_conns":["*internal"], + "scheduler_conns": ["*internal"] +}, + +"sessions": { + "enabled": true, + "listen_bijson": "127.0.0.1:2014", + "cdrs_conns": ["*internal"], + "stats_conns": ["*internal"], + "chargers_conns": ["*internal"], + "rals_conns": ["*internal"] +}, + +"chargers": { + "enabled": true, + "attributes_conns": ["*internal"] +}, + +"attributes": { + "enabled": true +}, + +"cdrs": { + "enabled": true, + "rals_conns": ["*internal"], + "stats_conns": ["*internal"], + "attributes_conns": ["*internal"] +} + +} + \ No newline at end of file diff --git a/data/conf/samples/fraud_mongo/cgrates.json b/data/conf/samples/fraud_mongo/cgrates.json new file mode 100644 index 000000000..3cdad30bc --- /dev/null +++ b/data/conf/samples/fraud_mongo/cgrates.json @@ -0,0 +1,90 @@ +{ + +"general": { + "log_level": 7, + "node_id": "CGR_FRAUD" +}, + + +"listen": { + "rpc_json": ":2012", + "rpc_gob": ":2013", + "http": ":2080" +}, + + +"data_db": { + "db_type": "mongo", + "db_name": "10", + "db_port": 27017 +}, + + +"stor_db": { + "db_type": "mongo", + "db_name": "cgrates", + "db_port": 27017 +}, + + +"rals": { + "enabled": true +}, + +"schedulers": { + "enabled": true +}, + +"filters": { + "stats_conns": ["*internal"], + "apiers_conns": ["*internal"] +}, + + +"stats": { + "enabled": true, + "indexed_selects":false, + "thresholds_conns": ["*internal"], + "store_interval": "1s" +}, + + +"thresholds": { + "enabled": true, + "indexed_selects":false, + "store_interval": "1s" +}, + +"apiers": { + "enabled": true, + "caches_conns":["*internal"], + "scheduler_conns": ["*internal"] +}, + +"sessions": { + "enabled": true, + "listen_bijson": "127.0.0.1:2014", + "cdrs_conns": ["*internal"], + "stats_conns": ["*internal"], + "chargers_conns": ["*internal"], + "rals_conns": ["*internal"] +}, + +"chargers": { + "enabled": true, + "attributes_conns": ["*internal"] +}, + +"attributes": { + "enabled": true +}, + +"cdrs": { + "enabled": true, + "rals_conns": ["*internal"], + "stats_conns": ["*internal"], + "attributes_conns": ["*internal"] +} + +} + \ No newline at end of file diff --git a/data/conf/samples/fraud_mysql/cgrates.json b/data/conf/samples/fraud_mysql/cgrates.json new file mode 100644 index 000000000..a555186bb --- /dev/null +++ b/data/conf/samples/fraud_mysql/cgrates.json @@ -0,0 +1,83 @@ +{ + +"general": { + "log_level": 7, + "node_id": "CGR_FRAUD" +}, + + +"listen": { + "rpc_json": ":2012", + "rpc_gob": ":2013", + "http": ":2080" +}, + +"data_db": { + "db_type": "redis", + "db_port": 6379, + "db_name": "10" +}, + +"stor_db": { + "db_password": "CGRateS.org" +}, + +"rals": { + "enabled": true +}, + +"schedulers": { + "enabled": true +}, + +"filters": { + "stats_conns": ["*internal"], + "apiers_conns": ["*internal"] +}, + +"stats": { + "enabled": true, + "indexed_selects":false, + "thresholds_conns": ["*internal"], + "store_interval": "1s" +}, + +"thresholds": { + "enabled": true, + "indexed_selects":false, + "store_interval": "1s" +}, + +"apiers": { + "enabled": true, + "caches_conns":["*internal"], + "scheduler_conns": ["*internal"] +}, + +"sessions": { + "enabled": true, + "listen_bijson": "127.0.0.1:2014", + "cdrs_conns": ["*internal"], + "stats_conns": ["*internal"], + "chargers_conns": ["*internal"], + "rals_conns": ["*internal"] +}, + +"chargers": { + "enabled": true, + "attributes_conns": ["*internal"] +}, + +"attributes": { + "enabled": true +}, + +"cdrs": { + "enabled": true, + "rals_conns": ["*internal"], + "stats_conns": ["*internal"], + "attributes_conns": ["*internal"] +} + +} + \ No newline at end of file diff --git a/general_tests/fraud_detection_it_test.go b/general_tests/fraud_detection_it_test.go new file mode 100644 index 000000000..454b809ab --- /dev/null +++ b/general_tests/fraud_detection_it_test.go @@ -0,0 +1,446 @@ +//go:build integration +// +build integration + +/* +Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments +Copyright (C) ITsysCOM GmbH + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +package general_tests + +import ( + "net/rpc" + "os" + "path" + "testing" + "time" + + "github.com/cgrates/cgrates/config" + "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/sessions" + "github.com/cgrates/cgrates/utils" +) + +var ( + fraudCfgPath string + fraudCfg *config.CGRConfig + fraudRPC *rpc.Client + fraudDelay int + fraudConfDIR string + + sTestsFraud = []func(t *testing.T){ + testFraudRemoveFolders, + testFraudCreateFolders, + + testFraudLoadConfig, + testFraudInitDataDb, + testFraudInitStorDb, + testFraudStartEngine, + testFraudRPCConn, + testFraudLoadTarriffPlans, + + testFraudAuthorizeandProcess1, + testFraudAuthorizeandProcess2, + testFraudAuthorizeandProcess3, + testFraudFinalAuthorize, + + testFraudStopEngine, + testFraudRemoveFolders, + } +) + +func TestFraudIT(t *testing.T) { + switch *dbType { + case utils.MetaInternal: + fraudConfDIR = "fraud_internal" + case utils.MetaMySQL: + fraudConfDIR = "fraud_mysql" + case utils.MetaMongo: + fraudConfDIR = "fraud_mongo" + case utils.MetaPostgres: + t.SkipNow() + default: + t.Fatal("Unknown Database type") + } + + for _, stest := range sTestsFraud { + t.Run(fraudConfDIR, stest) + } +} + +func testFraudCreateFolders(t *testing.T) { + if err := os.MkdirAll("/tmp/TestFraudIT", 0755); err != nil { + t.Error(err) + } +} + +func testFraudRemoveFolders(t *testing.T) { + if err := os.RemoveAll("/tmp/TestFraudIT"); err != nil { + t.Error(err) + } +} + +func testFraudLoadConfig(t *testing.T) { + var err error + fraudCfgPath = path.Join(*dataDir, "conf", "samples", fraudConfDIR) + if fraudCfg, err = config.NewCGRConfigFromPath(fraudCfgPath); err != nil { + t.Error(err) + } + fraudDelay = 1000 +} + +func testFraudInitDataDb(t *testing.T) { + if err := engine.InitDataDb(fraudCfg); err != nil { + t.Fatal(err) + } +} + +func testFraudInitStorDb(t *testing.T) { + if err := engine.InitStorDb(fraudCfg); err != nil { + t.Fatal(err) + } +} + +func testFraudStartEngine(t *testing.T) { + if _, err := engine.StopStartEngine(fraudCfgPath, fraudDelay); err != nil { + t.Fatal(err) + } +} + +func testFraudRPCConn(t *testing.T) { + var err error + fraudRPC, err = newRPCClient(fraudCfg.ListenCfg()) // We connect over JSON so we can also troubleshoot if needed + if err != nil { + t.Fatal("Could not connect to rater: ", err.Error()) + } +} + +func testFraudLoadTarriffPlans(t *testing.T) { + writeFile := func(fileName, data string) error { + csvFile, err := os.Create(path.Join("/tmp/TestFraudIT", fileName)) + if err != nil { + return err + } + defer csvFile.Close() + _, err = csvFile.WriteString(data) + if err != nil { + return err + + } + return csvFile.Sync() + } + + // Create and populate AccountActions.csv + if err := writeFile(utils.AccountActionsCsv, ` +#Tenant,Account,ActionPlanId,ActionTriggersId,AllowNegative,Disabled +cgrates.org,1001,AP_PACKAGE_10,,, +cgrates.org,1002,AP_PACKAGE_10,,, +`); err != nil { + t.Fatal(err) + } + + // Create and populate ActionPlans.csv + if err := writeFile(utils.ActionPlansCsv, ` +#Id,ActionsId,TimingId,Weight +AP_PACKAGE_10,ACT_TOPUP_RST_10,*asap,10 +`); err != nil { + t.Fatal(err) + } + + // Create and populate Actions.csv + if err := writeFile(utils.ActionsCsv, ` +#ActionsId[0],Action[1],ExtraParameters[2],Filter[3],BalanceId[4],BalanceType[5],Categories[6],DestinationIds[7],RatingSubject[8],SharedGroup[9],ExpiryTime[10],TimingIds[11],Units[12],BalanceWeight[13],BalanceBlocker[14],BalanceDisabled[15],Weight[16] +ACT_FRD_LOG,*log,,,,,,,,,,,,,,,0 +ACT_FRD_STOP,*disable_account,,,,,,,,,,,,,,,10 +ACT_TOPUP_RST_10,*topup_reset,,,test,*monetary,,*any,,,*unlimited,,10,10,false,false,10 +`); err != nil { + t.Fatal(err) + } + + // Create and populate Attributes.csv + if err := writeFile(utils.AttributesCsv, ` +#Tenant,ID,Context,FilterIDs,ActivationInterval,AttributeFilterIDs,Path,Type,Value,Blocker,Weight +cgrates.org,ATTR_FRD,*cdrs,,,,*opts.*account,*variable,~*req.Account,false,0 +`); err != nil { + t.Fatal(err) + } + + // cgrates.org,Raw,,,*raw,*constant:*req.RequestType:*none,0 + // Create and populate Chargers.csv + if err := writeFile(utils.ChargersCsv, ` +#Tenant,ID,FilterIDs,ActivationInterval,RunID,AttributeIDs,Weight +cgrates.org,DEFAULT,,,*default,*none,0 +`); err != nil { + t.Fatal(err) + } + + // Create and populate Destinations.csv + if err := writeFile(utils.DestinationsCsv, ` +#Id,Prefix +DST_1001,1001 +DST_1002,1002 +`); err != nil { + t.Fatal(err) + } + + // Create and populate DestinationRates.csv + if err := writeFile(utils.DestinationRatesCsv, ` +#Id,DestinationId,RatesTag,RoundingMethod,RoundingDecimals,MaxCost,MaxCostStrategy +DR_1001_20CNT,DST_1001,RT_20CNT,*up,4,0, +DR_1002_20CNT,DST_1002,RT_20CNT,*up,4,0, +`); err != nil { + t.Fatal(err) + } + + // Create and populate RatingPlans.csv + if err := writeFile(utils.RatingPlansCsv, ` +#Id,DestinationRatesId,TimingTag,Weight +RP_1001,DR_1002_20CNT,*any,10 +RP_1002,DR_1001_20CNT,*any,10 +`); err != nil { + t.Fatal(err) + } + + // Create and populate RatingProfiles.csv + if err := writeFile(utils.RatingProfilesCsv, ` +#Tenant,Category,Subject,ActivationTime,RatingPlanId,RatesFallbackSubject +cgrates.org,call,1001,2014-01-14T00:00:00Z,RP_1001, +cgrates.org,call,1002,2014-01-14T00:00:00Z,RP_1002, +`); err != nil { + t.Fatal(err) + } + + // Create and populate Rates.csv + if err := writeFile(utils.RatesCsv, ` +#Id,ConnectFee,Rate,RateUnit,RateIncrement,GroupIntervalStart +RT_20CNT,0.4,0.2,60s,60s,0s +RT_20CNT,0,0.1,60s,1s,60s +`); err != nil { + t.Fatal(err) + } + + // Create and populate Stats.csv + if err := writeFile(utils.StatsCsv, ` +#Tenant[0],Id[1],FilterIDs[2],ActivationInterval[3],QueueLength[4],TTL[5],MinItems[6],Metrics[7],MetricFilterIDs[8],Stored[9],Blocker[10],Weight[11],ThresholdIDs[12] +cgrates.org,STATS_FRD,,,-1,24h,0,*tcc,,true,false,0,THD_FRD +`); err != nil { + t.Fatal(err) + } + + // Create and populate Thresholds.csv + if err := writeFile(utils.ThresholdsCsv, ` +#Tenant[0],Id[1],FilterIDs[2],ActivationInterval[3],MaxHits[4],MinHits[5],MinSleep[6],Blocker[7],Weight[8],ActionIDs[9],Async[10] +cgrates.org,THD_FRD,*gte:~*req.*tcc:2,,-1,1,0,false,0,ACT_FRD_STOP;ACT_FRD_LOG,true +`); err != nil { + t.Fatal(err) + } + + var reply string + attrs := &utils.AttrLoadTpFromFolder{FolderPath: "/tmp/TestFraudIT"} + if err := fraudRPC.Call(utils.APIerSv1LoadTariffPlanFromFolder, attrs, &reply); err != nil { + t.Error(err) + } else if reply != utils.OK { + t.Error("Unexpected reply returned", reply) + } + time.Sleep(500 * time.Millisecond) +} + +func testFraudAuthorizeandProcess1(t *testing.T) { + originID := utils.GenUUID() + cgrEv := &utils.CGREvent{ + Tenant: "cgrates.org", + Event: map[string]interface{}{ + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.ToR: utils.MetaVoice, + utils.OriginID: originID, + utils.RequestType: utils.MetaPseudoPrepaid, + utils.AccountField: "1001", + utils.Subject: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2022, time.January, 7, 16, 60, 0, 0, time.UTC), + utils.AnswerTime: time.Date(2022, time.January, 7, 16, 60, 10, 0, time.UTC), + }, + APIOpts: map[string]interface{}{}, + } + args := sessions.NewV1AuthorizeArgs(false, []string{}, + false, []string{}, false, []string{}, false, true, + false, false, false, cgrEv, utils.Paginator{}, false, "") + + var rply sessions.V1AuthorizeReply + if err := fraudRPC.Call(utils.SessionSv1AuthorizeEvent, args, &rply); err != nil { + t.Error(err) + } + cgrEv = &utils.CGREvent{ + Tenant: "cgrates.org", + Event: map[string]interface{}{ + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.ToR: utils.MetaVoice, + utils.OriginID: originID, + utils.RequestType: utils.MetaPseudoPrepaid, + utils.AccountField: "1001", + utils.Subject: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2022, time.January, 7, 16, 60, 0, 0, time.UTC), + utils.AnswerTime: time.Date(2022, time.January, 7, 16, 60, 10, 0, time.UTC), + utils.Usage: 5 * time.Minute, + }, + APIOpts: map[string]interface{}{}, + } + var reply string + if err := fraudRPC.Call(utils.SessionSv1ProcessCDR, + cgrEv, &reply); err == nil || err.Error() != utils.ErrPartiallyExecuted.Error() { + t.Errorf("expected: <%+v>, \nreceived: <%+v>", utils.ErrPartiallyExecuted, err) + } +} + +func testFraudAuthorizeandProcess2(t *testing.T) { + originID := utils.GenUUID() + cgrEv := &utils.CGREvent{ + Tenant: "cgrates.org", + Event: map[string]interface{}{ + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.ToR: utils.MetaVoice, + utils.OriginID: originID, + utils.RequestType: utils.MetaPseudoPrepaid, + utils.AccountField: "1001", + utils.Subject: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2022, time.January, 7, 16, 60, 0, 0, time.UTC), + utils.AnswerTime: time.Date(2022, time.January, 7, 16, 60, 10, 0, time.UTC), + }, + APIOpts: map[string]interface{}{}, + } + args := sessions.NewV1AuthorizeArgs(false, []string{}, + false, []string{}, false, []string{}, false, true, + false, false, false, cgrEv, utils.Paginator{}, false, "") + + var rply sessions.V1AuthorizeReply + if err := fraudRPC.Call(utils.SessionSv1AuthorizeEvent, args, &rply); err != nil { + t.Error(err) + } + cgrEv = &utils.CGREvent{ + Tenant: "cgrates.org", + Event: map[string]interface{}{ + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.ToR: utils.MetaVoice, + utils.OriginID: originID, + utils.RequestType: utils.MetaPseudoPrepaid, + utils.AccountField: "1001", + utils.Subject: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2022, time.January, 7, 16, 60, 0, 0, time.UTC), + utils.AnswerTime: time.Date(2022, time.January, 7, 16, 60, 10, 0, time.UTC), + utils.Usage: 2 * time.Minute, + }, + APIOpts: map[string]interface{}{}, + } + var reply string + if err := fraudRPC.Call(utils.SessionSv1ProcessCDR, + cgrEv, &reply); err == nil || err.Error() != utils.ErrPartiallyExecuted.Error() { + t.Errorf("expected: <%+v>, \nreceived: <%+v>", utils.ErrPartiallyExecuted, err) + } +} + +func testFraudAuthorizeandProcess3(t *testing.T) { + originID := utils.GenUUID() + cgrEv := &utils.CGREvent{ + Tenant: "cgrates.org", + Event: map[string]interface{}{ + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.ToR: utils.MetaVoice, + utils.OriginID: originID, + utils.RequestType: utils.MetaPseudoPrepaid, + utils.AccountField: "1001", + utils.Subject: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2022, time.January, 7, 16, 60, 0, 0, time.UTC), + utils.AnswerTime: time.Date(2022, time.January, 7, 16, 60, 10, 0, time.UTC), + }, + APIOpts: map[string]interface{}{}, + } + args := sessions.NewV1AuthorizeArgs(false, []string{}, + false, []string{}, false, []string{}, false, true, + false, false, false, cgrEv, utils.Paginator{}, false, "") + + var rply sessions.V1AuthorizeReply + if err := fraudRPC.Call(utils.SessionSv1AuthorizeEvent, args, &rply); err != nil { + t.Error(err) + } + cgrEv = &utils.CGREvent{ + Tenant: "cgrates.org", + Event: map[string]interface{}{ + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.ToR: utils.MetaVoice, + utils.OriginID: originID, + utils.RequestType: utils.MetaPseudoPrepaid, + utils.AccountField: "1001", + utils.Subject: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2022, time.January, 7, 16, 60, 0, 0, time.UTC), + utils.AnswerTime: time.Date(2022, time.January, 7, 16, 60, 10, 0, time.UTC), + utils.Usage: 3 * time.Minute, + }, + APIOpts: map[string]interface{}{}, + } + var reply string + if err := fraudRPC.Call(utils.SessionSv1ProcessCDR, + cgrEv, &reply); err != nil { + t.Error(err) + } else if reply != utils.OK { + t.Errorf("Received reply: %s", reply) + } +} + +func testFraudFinalAuthorize(t *testing.T) { + cgrEv := &utils.CGREvent{ + Tenant: "cgrates.org", + Event: map[string]interface{}{ + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.ToR: utils.MetaVoice, + utils.OriginID: utils.GenUUID(), + utils.RequestType: utils.MetaPseudoPrepaid, + utils.AccountField: "1001", + utils.Subject: "1001", + utils.Destination: "1002", + utils.SetupTime: time.Date(2022, time.January, 7, 16, 60, 0, 0, time.UTC), + utils.AnswerTime: time.Date(2022, time.January, 7, 16, 60, 10, 0, time.UTC), + }, + APIOpts: map[string]interface{}{}, + } + args := sessions.NewV1AuthorizeArgs(false, []string{}, + false, []string{}, false, []string{}, false, true, + false, false, false, cgrEv, utils.Paginator{}, false, "") + + expErr := `RALS_ERROR:ACCOUNT_DISABLED` + var rply sessions.V1AuthorizeReply + if err := fraudRPC.Call(utils.SessionSv1AuthorizeEvent, args, + &rply); err == nil || err.Error() != expErr { + t.Error(err) + } +} + +func testFraudStopEngine(t *testing.T) { + if err := engine.KillEngine(fraudDelay); err != nil { + t.Error(err) + } +}