diff --git a/controllers/api/campaign.go b/controllers/api/campaign.go index 33c08fe7..eb0f9b6c 100644 --- a/controllers/api/campaign.go +++ b/controllers/api/campaign.go @@ -135,3 +135,18 @@ func (as *Server) CampaignComplete(w http.ResponseWriter, r *http.Request) { JSONResponse(w, models.Response{Success: true, Message: "Campaign completed successfully!"}, http.StatusOK) } } + +func (as *Server) FalsePositive(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, _ := strconv.ParseInt(vars["id"], 0, 64) + rid, _ := vars["rid"] + switch { + case r.Method == "GET": + err := models.MarkEvent(id, rid) + if err != nil { + JSONResponse(w, models.Response{Success: false, Message: "Error marking event as false positive"}, http.StatusInternalServerError) + return + } + JSONResponse(w, models.Response{Success: true, Message: "Event marked as false positive!"}, http.StatusOK) + } +} diff --git a/controllers/api/server.go b/controllers/api/server.go index 4e21cf4b..f9b49186 100644 --- a/controllers/api/server.go +++ b/controllers/api/server.go @@ -67,6 +67,7 @@ func (as *Server) registerRoutes() { router.HandleFunc("/campaigns/{id:[0-9]+}/results", as.CampaignResults) router.HandleFunc("/campaigns/{id:[0-9]+}/summary", as.CampaignSummary) router.HandleFunc("/campaigns/{id:[0-9]+}/complete", as.CampaignComplete) + router.HandleFunc("/falsepositive/{id:[0-9]+}/rid/{rid:[a-zA-Z0-9]+}", as.FalsePositive) router.HandleFunc("/groups/", as.Groups) router.HandleFunc("/groups/summary", as.GroupsSummary) router.HandleFunc("/groups/{id:[0-9]+}", as.Group) diff --git a/db/db_mysql/migrations/20210519000000_0.11.0_false_positiv.sql b/db/db_mysql/migrations/20210519000000_0.11.0_false_positiv.sql new file mode 100644 index 00000000..8745713c --- /dev/null +++ b/db/db_mysql/migrations/20210519000000_0.11.0_false_positiv.sql @@ -0,0 +1,6 @@ +-- +goose Up +-- SQL in section 'Up' is executed when this migration is applied +ALTER TABLE `events` ADD COLUMN false_positive BOOLEAN DEFAULT 0; + +-- +goose Down +-- SQL section 'Down' is executed when this migration is rolled back \ No newline at end of file diff --git a/db/db_sqlite3/migrations/20210519000000_0.11.0_false_positiv.sql b/db/db_sqlite3/migrations/20210519000000_0.11.0_false_positiv.sql new file mode 100644 index 00000000..1f6ca99a --- /dev/null +++ b/db/db_sqlite3/migrations/20210519000000_0.11.0_false_positiv.sql @@ -0,0 +1,6 @@ +-- +goose Up +-- SQL in section 'Up' is executed when this migration is applied +ALTER TABLE events ADD COLUMN false_positive BOOLEAN DEFAULT 0; + +-- +goose Down +-- SQL section 'Down' is executed when this migration is rolled back diff --git a/models/campaign.go b/models/campaign.go index 3867122f..e235751c 100644 --- a/models/campaign.go +++ b/models/campaign.go @@ -74,12 +74,13 @@ type CampaignStats struct { // Event contains the fields for an event // that occurs during the campaign type Event struct { - Id int64 `json:"-"` - CampaignId int64 `json:"campaign_id"` - Email string `json:"email"` - Time time.Time `json:"time"` - Message string `json:"message"` - Details string `json:"details"` + Id int64 `json:"id"` + CampaignId int64 `json:"campaign_id"` + Email string `json:"email"` + Time time.Time `json:"time"` + Message string `json:"message"` + Details string `json:"details"` + FalsePositive bool `json:"false_positive"` } // EventDetails is a struct that wraps common attributes we want to store @@ -154,6 +155,33 @@ func (c *Campaign) UpdateStatus(s string) error { return db.Table("campaigns").Where("id=?", c.Id).Update("status", s).Error } +// Sets the False Positive-field for the specified eventid to true. +// Checks if all Events with the specific email in that campaign where data has been submitted are set to false_positive "true" and if thats the case changes +// the status in the "results" table for that recipient back to "Clicked Link" hence it won´t be count in the Submitted statistic until new data is submitted +func MarkEvent(eveId int64, rid string) error { + var mailcount int64 + var fpcount int64 + s := Event{} + query := db.Table("events").Where("id=?", eveId).Update("false_positive", true).Error + if query != nil { + log.Errorf("Problem editing false_positive in database: Table \"events\" on id %d", eveId) + } + mailquery := db.Table("events").Where("id = ?", eveId) + mailquery.Select("id, campaign_id, email, time, message, details, false_positive") + err := mailquery.Scan(&s).Error + if err != nil { + log.Error(err) + return err + } + //Counter to check if all Data Submitted is flaged as false/positive + db.Table("events").Select("Count(*)").Where("email = ? AND message = ?", s.Email, "Submitted Data").Count(&mailcount) + db.Table("events").Select("Count(*)").Where("email = ? AND message = ? AND false_positive = ?", s.Email, "Submitted Data", true).Count(&fpcount) + if mailcount == fpcount { + return db.Table("results").Where("campaign_id = ? AND r_id = ?", s.CampaignId, rid).Update("status", "Clicked Link").Error + } + return query +} + // AddEvent creates a new campaign event in the database func AddEvent(e *Event, campaignID int64) error { e.CampaignId = campaignID diff --git a/static/css/main.css b/static/css/main.css index 42c3713e..5dceece9 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -741,4 +741,10 @@ table.dataTable { } #password-strength-container { height: 40px; +} +.ms-1 { + margin-left: 1em; +} +.mt-1 { + margin-top: 1em; } \ No newline at end of file diff --git a/static/js/src/app/campaign_results.js b/static/js/src/app/campaign_results.js index 0a198b9d..08684263 100644 --- a/static/js/src/app/campaign_results.js +++ b/static/js/src/app/campaign_results.js @@ -42,6 +42,12 @@ var statuses = { icon: "fa-exclamation", point: "ct-point-clicked" }, + "True/False":{ + color: "#6c7a89", + label: "label-info", + icon: "fa-eraser", + point: "ct-point-true_false" + }, //not a status, but is used for the campaign timeline and user timeline "Email Reported": { color: "#45d6ef", @@ -178,7 +184,7 @@ function completeCampaign() { return new Promise(function (resolve, reject) { api.campaignId.complete(campaign.id) .success(function (msg) { - resolve() + resolve() }) .error(function (data) { reject(data.responseJSON.message) @@ -236,6 +242,56 @@ function exportAsCSV(scope) { $("#exportButton").html(exportHTML) } +function false_positive(rid, eventid, cid, repo) { + if(repo === 'true'){ + Swal.fire({ + title: "Are you sure?", + text: "This will flag the submitted data as False Positive and substract it from the count. Please make sure that they are invalid!", + type: "question", + animation: false, + showCancelButton: true, + reverseButtons: true, + allowOutsideClick: false, + showLoaderOnConfirm: true + }).then(function (result) { + if (result.value){ + api.eventId.falsepositive(eventid, rid).success((function() { + refresh(); + } )); + } + }) + } + else{ // will be called if Reported is not marked + Swal.fire({ + title: "Are you sure?", + text: "This will flag the submitted data as False Positive and substract it from the count. Please make sure that they are invalid!", + type: "question", + animation: false, + showCancelButton: true, + input: 'checkbox', + inputValue: 1, + inputPlaceholder: 'Mark as reported too', + confirmButtonText: "Continue", + confirmButtonColor: "#428bca", + reverseButtons: true, + allowOutsideClick: false, + showLoaderOnConfirm: true + }).then(function (result) { + if (result.value && !result.reported){ // Reports the rid as Reported and the submitted data as false positive + report_mail(escapeHtml(rid), cid); + api.eventId.falsepositive(eventid, rid).success((function() { + refresh(); + } )); + } else if (result.value === 0){ // Marks the Event with the submitted data as false positive + api.eventId.falsepositive(eventid, rid).success((function() { + refresh(); + })); + } + }) + } +} + + function replay(event_idx) { request = campaign.timeline[event_idx] details = JSON.parse(request.details) @@ -400,7 +456,13 @@ function renderTimeline(data) { } if (event.message == "Submitted Data") { results += '
' + results += ' Replay Credentials' + if(campaign.timeline[i].false_positive === true){ + results += '