Commit 7a3efc28 authored by Rémi PAILHAREY's avatar Rémi PAILHAREY
Browse files

clean: tidy before code review

parent 6a4323d1
......@@ -11,6 +11,7 @@
"mode": "debug",
"program": "${workspaceFolder}",
"env": {
"LOG_FILE": "./log.txt",
"ADMIN_ROLE": "ADMINS",
"USER_ROLE": "USERS",
"INMEMORY_TOKEN_LIFE_DAYS": "2",
......@@ -22,7 +23,9 @@
"EMAIL_SENDER_ADDRESS": "jeanjestin@gmail.com",
"EMAIL_SENDER_PASSWORD": "tlpcxutxkfrjzfha",
"EMAIL_SMTP_SERVER": "smtp.gmail.com",
"EMAIL_SMTP_PORT": "587"
"EMAIL_SMTP_PORT": "587",
"SSI_EMAIL" : "remipailharey@gmail.com"
// "SSI_EMAIL" : "ssi-metropoledelyon@grandlyon.com"
}
}
]
......
{
"Token": "MpaxfF-x3iewHcbvlfulNkYI1oJs2a28MFTNhBz5_iM="
}
\ No newline at end of file
......@@ -17,16 +17,21 @@ import (
"forge.grandlyon.com/rpailharey/cyber-signal/internal/common"
)
var (
cuckooURL = common.StringValueFromEnv("CUCKOO_URL", "")
cuckooAuthToken = common.StringValueFromEnv("CUCKOO_AUTH_TOKEN", "")
)
//UploadResponse struct
type UploadResponse struct {
TaskID int `json:"task_id"`
}
var (
cuckooURL string
cuckooAuthToken string
)
func Init() {
cuckooURL = common.StringValueFromEnv("CUCKOO_URL", "")
cuckooAuthToken = common.StringValueFromEnv("CUCKOO_AUTH_TOKEN", "")
}
// Uploads a file to Cuckoo and requests an analysis
func SendPostRequestMultipart(filename string) int {
url := cuckooURL + "/tasks/create/file"
......@@ -52,7 +57,6 @@ func SendPostRequestMultipart(filename string) int {
if err != nil {
log.Fatal(err)
}
//fmt.Println("Task ID:", uploadResponse.TaskID)
return uploadResponse.TaskID
}
......@@ -60,7 +64,6 @@ func SendPostRequestMultipart(filename string) int {
// Get the analysis' report
func SendGetSummaryReport(taskid int) (report []byte, err error) {
url := fmt.Sprintf("%s/tasks/summary/%d", cuckooURL, taskid)
fmt.Println(url)
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
......
......@@ -6,19 +6,22 @@ import (
"html/template"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"time"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/common"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/cuckoo"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/log"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/mail"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/thehive"
)
var (
cybersignalAuthToken string
ackMailTemplate = "ackmailtemplate.html"
CybersignalAuthToken string
ackMailTemplate = "templates/ackmailtemplate.html"
resultMailTemplate = "templates/resultmailtemplate.html"
ssiMailTemplate = "templates/ssimailtemplate.html"
mailConfig = mail.EmailConfig{
Username: common.StringValueFromEnv("EMAIL_SENDER_ADDRESS", ""),
Password: common.StringValueFromEnv("EMAIL_SENDER_PASSWORD", ""),
......@@ -27,7 +30,7 @@ var (
SenderAddr: common.StringValueFromEnv("EMAIL_SENDER_ADDRESS", ""),
}
mailSender = mail.NewSender(mailConfig)
mailDest string
mailSSI = common.StringValueFromEnv("SSI_EMAIL", "")
)
func Init(keyfile string) {
......@@ -38,15 +41,15 @@ func Init(keyfile string) {
if err != nil {
tokenConfig.Token, err = common.GenerateRandomString(32)
if err != nil {
log.Fatal(err)
log.Logger.Fatal(err)
}
err := common.Save(keyfile, tokenConfig)
if err != nil {
log.Println("Authorization token could not be saved")
log.Logger.Println("Authorization token could not be saved")
}
}
log.Println("Authorization token set")
cybersignalAuthToken = string(tokenConfig.Token)
log.Logger.Println("Authorization token set")
CybersignalAuthToken = string(tokenConfig.Token)
}
func init() {
......@@ -56,75 +59,101 @@ func init() {
// HandleUpload is a function allowing to analyze a file with Cuckoo and save the incident in TheHive.
func HandleUpload() http.Handler {
handleUpload := func(w http.ResponseWriter, r *http.Request) {
// Check if body is empty
if r.Body == http.NoBody {
http.Error(w, "error body is empty", http.StatusBadRequest)
return
}
// TempFile allows you to create a new temp file in the /tmp directory
file, errTemp := ioutil.TempFile("", "mail*.msg")
if errTemp != nil {
panic(errTemp)
http.Error(w, errTemp.Error(), http.StatusInternalServerError)
return
}
// Copy the received file locally
io.Copy(file, r.Body)
file.Close()
// Send an email to confirm the file is in process
// TODO:
SendAcknowledgmentMail("remipailharey@gmail.com")
// Send the file to Cuckoo for an analysis
taskid := cuckoo.SendPostRequestMultipart(file.Name())
log.Println(taskid)
// Try to get the report every 30 seconds
var report []byte
var err error
for {
time.Sleep(30 * time.Second)
report, err = cuckoo.SendGetSummaryReport(taskid)
if err == nil {
break
}
fileBytes, err := ioutil.ReadFile(file.Name())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
detectedFileType := http.DetectContentType(fileBytes)
severity, senderEmail, subject, urls, sha1, md5, err := cuckoo.ExtractIOC(report)
if err != nil {
log.Fatal(err)
if detectedFileType != "application/octet-stream" {
http.Error(w, "error invalid file type", http.StatusBadRequest)
return
}
// Send an email to inform about the severity of the reported mail
// TODO:
w.Write([]byte("upload succeeded\n"))
// Create a new case
tags := []string{"Cyber-Signal"}
caseCreated, err := thehive.SendPostCreateCase(subject, "Test Cyber-Signal", thehive.RedTlp, severity, nil, tags, false, thehive.MetricField{})
if err != nil {
log.Fatal(err)
}
go func(userMail string, file *os.File) {
// Send an email to confirm the file is in process
sendAcknowledgmentMail(userMail)
// Add all observables
thehive.SendPostAddObservable(caseCreated.Id, senderEmail, "email-src", "L'adresse mail de l'expéditeur", thehive.RedTlp, tags, true)
thehive.SendPostAddObservable(caseCreated.Id, subject, "mail-subject", "L'objet du mail signalé", thehive.RedTlp, tags, true)
thehive.SendPostAddObservable(caseCreated.Id, sha1, "sha1", "Le hash sha1 du mail signalé", thehive.RedTlp, tags, true)
thehive.SendPostAddObservable(caseCreated.Id, md5, "md5", "Le hash md5 du mail signalé", thehive.RedTlp, tags, true)
for i := range urls {
thehive.SendPostAddObservable(caseCreated.Id, urls[i], "url", "Un lien présent dans le mail signalé", thehive.RedTlp, tags, true)
}
// Send the file to Cuckoo for an analysis
taskid := cuckoo.SendPostRequestMultipart(file.Name())
log.Logger.Printf("| New analysis submitted | report ID : %d | %v | %v", taskid, r.RemoteAddr, log.GetCityAndCountryFromRequest(r))
// Find linked cases
linkedCases, err := thehive.SendGetLinkedCases(caseCreated.Id)
if err != nil {
log.Fatal(err)
}
// Try to get the report every 30 seconds
var report []byte
for {
time.Sleep(30 * time.Second)
report, err = cuckoo.SendGetSummaryReport(taskid)
if err == nil {
break
}
}
// If there are linked cases, update their metric field
if len(linkedCases) != 0 {
for i := range linkedCases {
linkedCase := linkedCases[i]
thehive.SendPatchIncrementMetric(linkedCase)
severity, senderEmail, subject, urls, sha1, md5, err := cuckoo.ExtractIOC(report)
if err != nil {
log.Logger.Fatal(err)
}
} else {
// If there are no linked cases, send an email to the SSI team
// TODO:
fmt.Println("No linked cases!")
}
log.Logger.Printf("| Got results of report ID : %d | Severity : %d | %v | %v", taskid, severity, r.RemoteAddr, log.GetCityAndCountryFromRequest(r))
// Send an email to inform about the severity of the reported mail
sendResultMail(senderEmail, severity)
// Create a new case
tags := []string{"Cyber-Signal"}
caseCreated, err := thehive.SendPostCreateCase(subject, "Test Cyber-Signal", thehive.RedTlp, severity, nil, tags, false, thehive.MetricField{})
if err != nil {
log.Logger.Fatal(err)
}
// Add all observables
thehive.SendPostAddObservable(caseCreated.Id, senderEmail, "email-src", "L'adresse mail de l'expéditeur", thehive.RedTlp, tags, true)
thehive.SendPostAddObservable(caseCreated.Id, subject, "mail-subject", "L'objet du mail signalé", thehive.RedTlp, tags, true)
thehive.SendPostAddObservable(caseCreated.Id, sha1, "sha1", "Le hash sha1 du mail signalé", thehive.RedTlp, tags, true)
thehive.SendPostAddObservable(caseCreated.Id, md5, "md5", "Le hash md5 du mail signalé", thehive.RedTlp, tags, true)
for i := range urls {
thehive.SendPostAddObservable(caseCreated.Id, urls[i], "url", "Un lien présent dans le mail signalé", thehive.RedTlp, tags, true)
}
// Find linked cases
linkedCases, err := thehive.SendGetLinkedCases(caseCreated.Id)
if err != nil {
log.Logger.Fatal(err)
}
// If there are linked cases, update their metric field
if len(linkedCases) != 0 {
for i := range linkedCases {
linkedCase := linkedCases[i]
thehive.SendPatchIncrementMetric(linkedCase)
}
log.Logger.Printf("| Incident created with similar cases | Case ID : %v | %v | %v", caseCreated.Id, r.RemoteAddr, log.GetCityAndCountryFromRequest(r))
} else {
// If there are no linked cases, send an email to the SSI team
sendSSIMail(caseCreated.Id, severity)
log.Logger.Printf("| Incident created with no similar cases | Case ID : %v | %v | %v", caseCreated.Id, r.RemoteAddr, log.GetCityAndCountryFromRequest(r))
}
}("remipailharey@gmail.com", file)
}
return http.HandlerFunc(handleUpload)
......@@ -134,15 +163,17 @@ func HandleUpload() http.Handler {
func ValidateAuthMiddleware(next http.Handler) http.Handler {
tokenChecker := func(w http.ResponseWriter, r *http.Request) {
authToken := r.Header.Get("Authorization")
if authToken != "Bearer "+cybersignalAuthToken {
if authToken != "Bearer "+CybersignalAuthToken {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("error invalid authorization token"))
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(tokenChecker)
}
func SendAcknowledgmentMail(email string) error {
func sendAcknowledgmentMail(email string) error {
// Receiver email address.
to := []string{
email,
......@@ -160,3 +191,88 @@ func SendAcknowledgmentMail(email string) error {
// Sending email.
return mailSender.Send(to, body.Bytes())
}
func sendResultMail(email string, severity int) error {
// Receiver email address.
to := []string{
email,
}
t, _ := template.ParseFiles(resultMailTemplate)
var body bytes.Buffer
mimeHeaders := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
body.Write([]byte(fmt.Sprintf("Subject: Résultat de l'analyse\n%s\n\n", mimeHeaders)))
var summary string
switch severity {
case thehive.LowSeverity:
summary = "Faible risque"
case thehive.MediumSeverity:
summary = "Risque modéré"
case thehive.HighSeverity:
summary = "Risque élevé"
case thehive.CriticalSeverity:
summary = "Risque critique"
}
t.Execute(&body, struct {
Summary string
}{
Summary: summary,
})
// Sending email.
return mailSender.Send(to, body.Bytes())
}
func sendSSIMail(caseId string, severity int) error {
// Receiver email address.
to := []string{
mailSSI,
}
t, _ := template.ParseFiles(ssiMailTemplate)
var body bytes.Buffer
mimeHeaders := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
body.Write([]byte(fmt.Sprintf("Subject: Nouvel incident créé (#%s)\n%s\n\n", caseId, mimeHeaders)))
var summary string
switch severity {
case thehive.LowSeverity:
summary = "Faible risque"
case thehive.MediumSeverity:
summary = "Risque modéré"
case thehive.HighSeverity:
summary = "Risque élevé"
case thehive.CriticalSeverity:
summary = "Risque critique"
}
t.Execute(&body, struct {
TheHiveURL string
CaseId string
Summary string
}{
TheHiveURL: common.StringValueFromEnv("THEHIVE_URL", ""),
CaseId: caseId,
Summary: summary,
})
// Sending email.
return mailSender.Send(to, body.Bytes())
}
// SetTestMailSender allows overriding mail sender for test purposes
func SetTestModeAndReturnRecorder() *mail.EmailRecorder {
ackMailTemplate = "../../templates/ackmailtemplate.html"
resultMailTemplate = "../../templates/resultmailtemplate.html"
ssiMailTemplate = "../../templates/ssimailtemplate.html"
sender, recorder := mail.NewMockSender()
mailSender = sender
return recorder
}
package cybersignal
import (
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"regexp"
"testing"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/mocks"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/tester"
)
var noH map[string]string
// Test the use case described in the README file
func TestUseCase(t *testing.T) {
_, doCuckoo := createCuckooTester(t)
_, doTheHive := createTheHiveTester(t)
// Submit a file to analysis
resp := doCuckoo("POST", "/tasks/create/file", noH, "", http.StatusOK, "")
// Extract report id
resp = regexp.MustCompile(`"task_id":(.*)`).FindStringSubmatch(resp)[1]
// Get report
resp = doCuckoo("GET", "/tasks/summary/"+resp, noH, "", http.StatusOK, "")
doTheHive("GET", "/api/case", noH, "", http.StatusOK, "")
}
func createCuckooTester(t *testing.T) (*httptest.Server, func(method string, route string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string) {
// Create the server
mux := mocks.CreateMockCuckooAPI()
ts := httptest.NewServer(mux)
url, _ := url.Parse(ts.URL)
port := url.Port()
// Create the cookie jar
jar, _ := cookiejar.New(nil)
// wrap the testing function
return ts, tester.CreateServerTester(t, port, jar)
}
func createTheHiveTester(t *testing.T) (*httptest.Server, func(method string, route string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string) {
// Create the server
mux := mocks.CreateMockTheHiveAPI()
ts := httptest.NewServer(mux)
url, _ := url.Parse(ts.URL)
port := url.Port()
// Create the cookie jar
jar, _ := cookiejar.New(nil)
// wrap the testing function
return ts, tester.CreateServerTester(t, port, jar)
}
......@@ -9,19 +9,30 @@ import (
func CreateMockTheHiveAPI() *http.ServeMux {
mux := http.NewServeMux()
// POST Create new case
// GET => return a case or POST => create a new case
mux.HandleFunc("/api/case", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "POST":
w.WriteHeader(http.StatusCreated)
case "GET":
w.WriteHeader(http.StatusOK)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{
"_id":"7917792",
"id":"7917792",
"_id":"1",
"id":"1",
"createdBy":"rpailharey@grandlyon.com",
"updatedBy":null,
"createdAt":1615283443807,
"updatedAt":null,
"_type":"case",
"caseId":141,
"title":"Test",
"title":"Premier incident",
"description":"Courte description",
"severity":1,
"startDate":1615283443665,
......@@ -40,7 +51,7 @@ func CreateMockTheHiveAPI() *http.ServeMux {
})
// POST Create new observable
mux.HandleFunc("/api/case/0/artifact", func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc("/api/case/1/artifact", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{
"id":"123456",
......@@ -58,8 +69,38 @@ func CreateMockTheHiveAPI() *http.ServeMux {
}`))
})
// GET Get linked cases
mux.HandleFunc("/api/case/1/links", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{[
"_id":"2",
"id":"2",
"createdBy":"rpailharey@grandlyon.com",
"updatedBy":null,
"createdAt":1615283443807,
"updatedAt":null,
"_type":"case",
"caseId":141,
"title":"Deuxième incident",
"description":"Courte description",
"severity":1,
"startDate":1615283443665,
"endDate":null,
"impactStatus":null,
"resolutionStatus":null,
"tags":["Test Cyber-Signal"],
"flag":false,"tlp":1,"pap":2,
"status":"Open",
"summary":null,
"owner":"rpailharey@grandlyon.com",
"customFields":{},
"stats":{},
"permissions":["manageShare","manageAnalyse","manageTask","manageCaseTemplate","manageCase","manageUser","managePage","manageObservable","manageConfig","manageAlert","manageAction"]
]}`))
})
// DELETE Delete a case
mux.HandleFunc("/api/case/0", func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc("/api/case/1", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
......
......@@ -2,33 +2,108 @@ package rootmux
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/auth"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/cuckoo"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/cybersignal"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/mocks"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/tester"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/thehive"
"forge.grandlyon.com/rpailharey/cyber-signal/internal/tokens"
)
var (
newUser = `{"id":"3","login":"new_user","memberOf":["USERS"],"password":"test"}`
noH map[string]string
newUser = `{"id":"3","login":"new_user","memberOf":["USERS"],"password":"test"}`
noH map[string]string
authorizationHeader map[string]string
)
func init() {
tokens.Init("../../configs/tokenskey.json", true)
cybersignal.Init("../../authtoken.json")
authorizationHeader = make(map[string]string)
authorizationHeader["Authorization"] = "Bearer " + cybersignal.CybersignalAuthToken
}
func TestAll(t *testing.T) {
func TestMain(m *testing.M) {
// Create the Cuckoo mock server
cuckooServer := httptest.NewServer(mocks.CreateMockCuckooAPI())
defer cuckooServer.Close()
// Setup to use the cuckoo mock server
os.Setenv("CUCKOO_URL", cuckooServer.URL)
// Create the Cuckoo mock server
theHiveServer := httptest.NewServer(mocks.CreateMockTheHiveAPI())
defer theHiveServer.Close()
// Setup to use the cuckoo mock server
os.Setenv("THEHIVE_URL", theHiveServer.URL)
thehive.Init()
cuckoo.Init()
os.Exit(m.Run())
}
func TestUseCase(t *testing.T) {
ts, do := createTester(t)
defer ts.Close()
recorder := cybersignal.SetTestModeAndReturnRecorder()
// Try to upload a file without the authorization token (must fail)
do("POST", "/upload", noH, "", http.StatusUnauthorized, "error invalid authorization token")
// // Check that no mails were sent
tester.AssertEqual(t, recorder.Msg(), "")
// Try to upload an empty file (must fail)
do("POST", "/upload", authorizationHeader, "", http.StatusBadRequest, "error body is empty")
// // Check that no mails were sent
tester.AssertEqual(t, recorder.Msg(), "")
// Try to upload a file without the authorization token (must fail)
do("POST", "/upload", authorizationHeader, "invalid body content", http.StatusBadRequest, "error invalid file type")
// // Check that no mails were sent
tester.AssertEqual(t, recorder.Msg(), "")
fileBytes, err := ioutil.ReadFile("../../testmail.msg")
if err != nil {
t.Error(err)
}