diff options
author | Max Magorsch <arzano@gentoo.org> | 2020-04-18 02:38:35 +0200 |
---|---|---|
committer | Max Magorsch <arzano@gentoo.org> | 2020-04-18 02:50:54 +0200 |
commit | 35a41e63ebd5f6cf9d17419c150eb53a005d2e87 (patch) | |
tree | e0bcc21bbb1e7e200857cfbd52acb82b008a3a6d /pkg | |
parent | Display version and last update in the footer (diff) | |
download | glsamaker-35a41e63ebd5f6cf9d17419c150eb53a005d2e87.tar.gz glsamaker-35a41e63ebd5f6cf9d17419c150eb53a005d2e87.tar.bz2 glsamaker-35a41e63ebd5f6cf9d17419c150eb53a005d2e87.zip |
Add the initial version of the rewritten glsamaker
The glsamaker has been completly rewritten in go. It is
using postgres instead of mysql now. The look and feel is
based on tyrian.
Signed-off-by: Max Magorsch <arzano@gentoo.org>
Diffstat (limited to 'pkg')
68 files changed, 5161 insertions, 0 deletions
diff --git a/pkg/app/handler/about/index.go b/pkg/app/handler/about/index.go new file mode 100644 index 0000000..edeb45b --- /dev/null +++ b/pkg/app/handler/about/index.go @@ -0,0 +1,28 @@ +// Used to show the about pages of the application + +package about + +import ( + "glsamaker/pkg/app/handler/authentication/utils" + "net/http" +) + +// Show renders a template to show the main about page of the application +func Show(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + renderAboutTemplate(w, user) +} + +// ShowSearch renders a template to show the about +// page about the search functionality +func ShowSearch(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + renderAboutSearchTemplate(w, user) +} + +// ShowCLI renders a template to show the about +// page about the command line tool +func ShowCLI(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + renderAboutCLITemplate(w, user) +} diff --git a/pkg/app/handler/about/utils.go b/pkg/app/handler/about/utils.go new file mode 100644 index 0000000..77f1383 --- /dev/null +++ b/pkg/app/handler/about/utils.go @@ -0,0 +1,58 @@ +// miscellaneous utility functions used for the about pages of the application + +package about + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderAboutTemplate renders all templates used for the main about page +func renderAboutTemplate(w http.ResponseWriter, user *users.User) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/about/*.tmpl")) + + templates.ExecuteTemplate(w, "about.tmpl", createPageData("about", user)) +} + +// renderAboutSearchTemplate renders all templates used for +// the about page about the search functionality +func renderAboutSearchTemplate(w http.ResponseWriter, user *users.User) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/about/*.tmpl")) + + templates.ExecuteTemplate(w, "aboutSearch.tmpl", createPageData("about", user)) +} + +// renderAboutCLITemplate renders all templates used for +// the about page about the command line tool +func renderAboutCLITemplate(w http.ResponseWriter, user *users.User) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/about/*.tmpl")) + + templates.ExecuteTemplate(w, "aboutCLI.tmpl", createPageData("about", user)) +} + +// createPageData creates the data used in the templates of the about pages +func createPageData(page string, user *users.User) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + } +} diff --git a/pkg/app/handler/account/password.go b/pkg/app/handler/account/password.go new file mode 100644 index 0000000..5cdb9e5 --- /dev/null +++ b/pkg/app/handler/account/password.go @@ -0,0 +1,109 @@ +// Used to show the change password page + +package account + +import ( + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// ChangePassword changes the password of a user in case of a valid POST request. +// In case of a GET request the dialog for the password change is displayed +func ChangePassword(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if r.Method == "POST" { + + r.ParseForm() + + oldPassword := getStringParam("oldPassword", r) + newPassword := getStringParam("newPassword", r) + confirmedNewPassword := getStringParam("confirmedNewPassword", r) + + if newPassword != confirmedNewPassword { + renderPasswordChangeTemplate(w, r, user, false, "The passwords you have entered do not match") + return + } + + if !user.CheckPassword(oldPassword) { + renderPasswordChangeTemplate(w, r, user, false, "The old password you have entered is not correct") + return + } + + err := user.UpdatePassword(newPassword) + if err != nil { + renderPasswordChangeTemplate(w, r, user, false, "Internal error during hash calculation.") + return + } + + wasForcedToChange := user.ForcePasswordRotation + user.ForcePasswordRotation = false + + _, err = connection.DB.Model(user).Column("password").WherePK().Update() + _, err = connection.DB.Model(user).Column("force_password_rotation").WherePK().Update() + + if err != nil { + logger.Info.Println("error during password update") + logger.Info.Println(err) + renderPasswordChangeTemplate(w, r, user, false, "Internal error during password update.") + return + } + + if wasForcedToChange { + http.Redirect(w, r, "/", 301) + return + } + + updatedUser := utils.GetAuthenticatedUser(r) + + renderPasswordChangeTemplate(w, r, updatedUser, true, "Your password has been changed successfully.") + return + } + + renderPasswordChangeTemplate(w, r, user, false, "") +} + +// renderPasswordChangeTemplate renders all templates used for the login page +func renderPasswordChangeTemplate(w http.ResponseWriter, r *http.Request, user *users.User, success bool, message string) { + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/account/password/*.tmpl")) + + templates.ExecuteTemplate(w, "password.tmpl", createPasswordChangeData("account", user, success, message)) +} + +// createPasswordChangeData creates the data used in the template of the password change page +func createPasswordChangeData(page string, user *users.User, success bool, message string) interface{} { + + return struct { + Page string + Application *models.GlobalSettings + User *users.User + Success bool + Message string + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + Success: success, + Message: message, + } +} + +// returns the value of a parameter with the given key of a POST request +func getStringParam(key string, r *http.Request) string { + if len(r.Form[key]) > 0 { + return r.Form[key][0] + } + + return "" +} diff --git a/pkg/app/handler/account/twofactor.go b/pkg/app/handler/account/twofactor.go new file mode 100644 index 0000000..4d87426 --- /dev/null +++ b/pkg/app/handler/account/twofactor.go @@ -0,0 +1,200 @@ +package account + +import ( + "glsamaker/pkg/app/handler/authentication/totp" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "bytes" + "github.com/duo-labs/webauthn/webauthn" + "html/template" + "net/http" +) + +// landing page + +func TwoFactorAuth(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + render2FATemplate(w, r, user) +} + +// webauthn + +func ActivateWebAuthn(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + + if user.WebauthnCredentials != nil && len(user.WebauthnCredentials) >= 0 { + updatedUser := &users.User{ + Id: user.Id, + IsUsingTOTP: false, + IsUsingWebAuthn: true, + Show2FANotice: false, + } + + _, err := connection.DB.Model(updatedUser).Column("is_using_totp").WherePK().Update() + _, err = connection.DB.Model(updatedUser).Column("is_using_web_authn").WherePK().Update() + _, err = connection.DB.Model(updatedUser).Column("show2fa_notice").WherePK().Update() + + if err != nil { + logger.Error.Println("Error activating webauthn") + logger.Error.Println(err) + } + + } + + http.Redirect(w, r, "/account/2fa", 301) +} + +func DisableWebAuthn(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + updatedUser := &users.User{ + Id: user.Id, + IsUsingWebAuthn: false, + } + + _, err := connection.DB.Model(updatedUser).Column("is_using_web_authn").WherePK().Update() + + if err != nil { + logger.Error.Println("Error disabling webauthn") + logger.Error.Println(err) + } + + http.Redirect(w, r, "/account/2fa", 301) +} + +// totp + +func ActivateTOTP(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + + updatedUser := &users.User{ + Id: user.Id, + IsUsingTOTP: true, + IsUsingWebAuthn: false, + Show2FANotice: false, + } + + _, err := connection.DB.Model(updatedUser).Column("is_using_totp").WherePK().Update() + _, err = connection.DB.Model(updatedUser).Column("is_using_web_authn").WherePK().Update() + _, err = connection.DB.Model(updatedUser).Column("show2fa_notice").WherePK().Update() + + if err != nil { + logger.Error.Println("Error activating totp") + logger.Error.Println(err) + } + + http.Redirect(w, r, "/account/2fa", 301) +} + +func DisableTOTP(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + + updatedUser := &users.User{ + Id: user.Id, + IsUsingTOTP: false, + } + + _, err := connection.DB.Model(updatedUser).Column("is_using_totp").WherePK().Update() + + if err != nil { + logger.Error.Println("Error updating 2fa") + logger.Error.Println(err) + } + + http.Redirect(w, r, "/account/2fa", 301) +} + +func VerifyTOTP(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + token := getToken(r) + + validToken := "false" + + if totp.IsValidTOTPToken(user, token) { + validToken = "true" + } + + w.Write([]byte(validToken)) +} + +func Disable2FANotice(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + updatedUser := &users.User{ + Id: user.Id, + Show2FANotice: false, + } + + _, err := connection.DB.Model(updatedUser).Column("show2fa_notice").WherePK().Update() + + if err != nil { + logger.Error.Println("Error disabling 2fa notice") + logger.Error.Println(err) + } + + w.Write([]byte("ok")) +} + +// utility functions + +func getToken(r *http.Request) string { + err := r.ParseForm() + if err != nil { + return "" + } + return r.Form.Get("token") +} + +// renderIndexTemplate renders all templates used for the login page +func render2FATemplate(w http.ResponseWriter, r *http.Request, user *users.User) { + + funcMap := template.FuncMap{ + "WebAuthnID": WebAuthnCredentialID, + "CredentialName": GetCredentialName, + } + + templates := template.Must( + template.Must( + template.New("Show"). + Funcs(funcMap). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/account/*.tmpl")) + + templates.ExecuteTemplate(w, "2fa.tmpl", createPageData("account", user)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User) interface{} { + return struct { + Page string + Application *models.GlobalSettings + QRcode string + User *users.User + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + QRcode: user.TOTPQRCode, + User: user, + } +} + +// WebAuthnCredentials returns credentials owned by the user +func WebAuthnCredentialID(cred webauthn.Credential) []byte { + return cred.ID[:5] +} + +func GetCredentialName(user *users.User, cred webauthn.Credential) string { + + for _, WebauthnCredentialName := range user.WebauthnCredentialNames { + if bytes.Compare(WebauthnCredentialName.Id, cred.ID) == 0 { + return WebauthnCredentialName.Name + } + } + + return "Unnamed Authenticator" +} diff --git a/pkg/app/handler/admin/edit.go b/pkg/app/handler/admin/edit.go new file mode 100644 index 0000000..8cf9291 --- /dev/null +++ b/pkg/app/handler/admin/edit.go @@ -0,0 +1,293 @@ +package admin + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/totp" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models/users" + "math/rand" + "net/http" + "strconv" + "strings" + "time" +) + +// Show renders a template to show the landing page of the application +func EditUsers(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Admin.ManageUsers { + authentication.AccessDenied(w, r) + return + } + + var allUsers []*users.User + connection.DB.Model(&allUsers).Order("email ASC").Select() + + if r.Method == "POST" { + + r.ParseForm() + + if !(getStringParam("edit", r) == "1") { + http.Redirect(w, r, "/admin", 301) + } + + userIds := getArrayParam("userId", r) + userNicks := getArrayParam("userNick", r) + userNames := getArrayParam("userName", r) + userEmails := getArrayParam("userEmail", r) + userPasswordRotations := getArrayParam("userPasswordRotation", r) + userForce2FA := getArrayParam("userForce2FA", r) + userActive := getArrayParam("userActive", r) + + newUserIndex := -1 + + for index, userId := range userIds { + + parsedUserId, err := strconv.ParseInt(userId, 10, 64) + + if err != nil { + continue + } + + count, _ := connection.DB.Model((*users.User)(nil)).Where("id = ?", parsedUserId).Count() + + // user is present + if count == 1 { + + updatedUser := users.User{ + Id: parsedUserId, + Email: userEmails[index], + Nick: userNicks[index], + Name: userNames[index], + //Badge: users.Badge{}, + ForcePasswordRotation: containsStr(userPasswordRotations, userId), + Force2FA: containsStr(userForce2FA, userId), + Disabled: !containsStr(userActive, userId), + } + + connection.DB.Model(&updatedUser). + Column("email"). + Column("nick"). + Column("name"). + Column("force_password_rotation"). + Column("force2fa"). + Column("disabled"). + WherePK().Update() + + } else { + + newUserIndex = index + + } + + } + + if newUserIndex != -1 { + + newPassword := generateNewPassword(14) + + createNewUser( + userNicks[newUserIndex], + userNames[newUserIndex], + userEmails[newUserIndex], + newPassword, + containsStr(userForce2FA, "-1"), + !containsStr(userActive, "-1")) + + var updatedUsers []*users.User + connection.DB.Model(&updatedUsers).Order("email ASC").Select() + + renderAdminNewUserTemplate(w, user, updatedUsers, userNicks[newUserIndex], newPassword) + return + + } else { + + http.Redirect(w, r, "/admin", 301) + return + + } + + } + + renderEditUsersTemplate(w, user, allUsers) +} + +// Show renders a template to show the landing page of the application +func EditPermissions(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Admin.ManageUsers { + authentication.AccessDenied(w, r) + return + } + + var allUsers []*users.User + connection.DB.Model(&allUsers).Order("email ASC").Select() + + if r.Method == "POST" { + + r.ParseForm() + + if !(getStringParam("edit", r) == "1") { + http.Redirect(w, r, "/admin", 301) + } + + glsaView := getArrayParam("glsa-view", r) + glsaUpdateBugs := getArrayParam("glsa-updateBugs", r) + glsaComment := getArrayParam("glsa-comment", r) + glsaCreate := getArrayParam("glsa-create", r) + glsaEdit := getArrayParam("glsa-edit", r) + glsaDelete := getArrayParam("glsa-delete", r) + glsaApprove := getArrayParam("glsa-approve", r) + glsaApproveOwnGlsa := getArrayParam("glsa-approveOwnGlsa", r) + glsaDecline := getArrayParam("glsa-decline", r) + glsaRelease := getArrayParam("glsa-release", r) + glsaConfidential := getArrayParam("glsa-confidential", r) + + cveView := getArrayParam("cve-view", r) + cveUpdateCVEs := getArrayParam("cve-updateCVEs", r) + cveComment := getArrayParam("cve-comment", r) + cveAddPackage := getArrayParam("cve-addPackage", r) + cveChangeState := getArrayParam("cve-changeState", r) + cveAssignBug := getArrayParam("cve-assignBug", r) + + adminView := getArrayParam("admin-view", r) + adminCreateTemplates := getArrayParam("admin-createTemplates", r) + adminGlobalSettings := getArrayParam("admin-globalSettings", r) + adminManageUsers := getArrayParam("admin-manageUsers", r) + + for _, changedUser := range allUsers { + + updatedUserPermissions := users.Permissions{ + Glsa: users.GlsaPermissions{ + View: containsInt(glsaView, changedUser.Id), + UpdateBugs: containsInt(glsaUpdateBugs, changedUser.Id), + Comment: containsInt(glsaComment, changedUser.Id), + Create: containsInt(glsaCreate, changedUser.Id), + Edit: containsInt(glsaEdit, changedUser.Id), + Approve: containsInt(glsaApprove, changedUser.Id), + ApproveOwnGlsa: containsInt(glsaApproveOwnGlsa, changedUser.Id), + Decline: containsInt(glsaDecline, changedUser.Id), + Delete: containsInt(glsaDelete, changedUser.Id), + Release: containsInt(glsaRelease, changedUser.Id), + Confidential: containsInt(glsaConfidential, changedUser.Id), + }, + CVETool: users.CVEToolPermissions{ + View: containsInt(cveView, changedUser.Id), + UpdateCVEs: containsInt(cveUpdateCVEs, changedUser.Id), + Comment: containsInt(cveComment, changedUser.Id), + AddPackage: containsInt(cveAddPackage, changedUser.Id), + ChangeState: containsInt(cveChangeState, changedUser.Id), + AssignBug: containsInt(cveAssignBug, changedUser.Id), + }, + Admin: users.AdminPermissions{ + View: containsInt(adminView, changedUser.Id), + CreateTemplates: containsInt(adminCreateTemplates, changedUser.Id), + ManageUsers: containsInt(adminManageUsers, changedUser.Id), + GlobalSettings: containsInt(adminGlobalSettings, changedUser.Id), + }, + } + + updatedUser := users.User{ + Id: changedUser.Id, + Permissions: updatedUserPermissions, + } + + connection.DB.Model(&updatedUser).Column("permissions").WherePK().Update() + } + + http.Redirect(w, r, "/admin", 301) + return + } + + renderEditPermissionsTemplate(w, user, allUsers) +} + +func containsInt(arr []string, element int64) bool { + return containsStr(arr, strconv.FormatInt(element, 10)) +} + +func containsStr(arr []string, element string) bool { + for _, a := range arr { + if a == element { + return true + } + } + return false +} + +func getStringParam(key string, r *http.Request) string { + if len(r.Form[key]) > 0 { + return r.Form[key][0] + } + + return "" +} + +func getArrayParam(key string, r *http.Request) []string { + return r.Form[key] +} + +func createNewUser(nick, name, email, password string, force2FA, disabled bool) { + + token, qrcode := totp.Generate("user@gentoo.org") + + badge := users.Badge{ + Name: "user", + Description: "Normal user", + Color: "#54487A", + } + + passwordParameters := users.Argon2Parameters{ + Type: "argon2id", + Time: 1, + Memory: 64 * 1024, + Threads: 4, + KeyLen: 32, + } + passwordParameters.GenerateSalt(32) + passwordParameters.GeneratePassword(password) + + defaultUser := &users.User{ + Email: email, + Nick: nick, + Name: name, + Password: passwordParameters, + Role: "user", + ForcePasswordChange: false, + TOTPSecret: token, + TOTPQRCode: qrcode, + IsUsingTOTP: false, + WebauthnCredentials: nil, + IsUsingWebAuthn: false, + Show2FANotice: true, + Badge: badge, + Disabled: disabled, + ForcePasswordRotation: true, + Force2FA: force2FA, + } + + _, err := connection.DB.Model(defaultUser).OnConflict("(id) DO Nothing").Insert() + if err != nil { + logger.Error.Println("Err during creating default admin user") + logger.Error.Println(err) + } +} + +func generateNewPassword(length int) string { + rand.Seed(time.Now().UnixNano()) + chars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZÅÄÖ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789" + + "!&!$%&/()=?") + var b strings.Builder + for i := 0; i < length; i++ { + b.WriteRune(chars[rand.Intn(len(chars))]) + } + return b.String() +} diff --git a/pkg/app/handler/admin/index.go b/pkg/app/handler/admin/index.go new file mode 100644 index 0000000..5f1f579 --- /dev/null +++ b/pkg/app/handler/admin/index.go @@ -0,0 +1,27 @@ +// Used to show the landing page of the application + +package admin + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models/users" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Admin.View { + authentication.AccessDenied(w, r) + return + } + + var users []*users.User + connection.DB.Model(&users).Order("email ASC").Select() + + renderAdminTemplate(w, user, users) +} diff --git a/pkg/app/handler/admin/passwordreset.go b/pkg/app/handler/admin/passwordreset.go new file mode 100644 index 0000000..e4d36b9 --- /dev/null +++ b/pkg/app/handler/admin/passwordreset.go @@ -0,0 +1,73 @@ +package admin + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models/users" + "net/http" + "strconv" +) + +// Show renders a template to show the landing page of the application +func ResetPassword(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Admin.ManageUsers { + authentication.AccessDenied(w, r) + return + } + + userPasswordResetId := r.URL.Path[len("/admin/edit/password/reset/"):] + + parsedUserPasswordResetId, err := strconv.ParseInt(userPasswordResetId, 10, 64) + + if err != nil { + http.NotFound(w, r) + return + } + + selectedUser := &users.User{Id: parsedUserPasswordResetId} + err = connection.DB.Model(selectedUser).WherePK().Select() + + if err != nil || selectedUser == nil { + http.NotFound(w, r) + return + } + + if r.Method == "POST" { + + newPassword := generateNewPassword(14) + passwordParameters := users.Argon2Parameters{ + Type: "argon2id", + Time: 1, + Memory: 64 * 1024, + Threads: 4, + KeyLen: 32, + } + passwordParameters.GenerateSalt(32) + passwordParameters.GeneratePassword(newPassword) + + updatedUser := &users.User{ + Id: parsedUserPasswordResetId, + Password: passwordParameters, + ForcePasswordRotation: true, + } + + _, err = connection.DB.Model(updatedUser).Column("password").WherePK().Update() + _, err = connection.DB.Model(updatedUser).Column("force_password_rotation").WherePK().Update() + if err != nil { + http.NotFound(w, r) + return + } + + var updatedUsers []*users.User + connection.DB.Model(&updatedUsers).Order("email ASC").Select() + + renderAdminNewUserTemplate(w, user, updatedUsers, selectedUser.Nick, newPassword) + return + } + + renderPasswordResetTemplate(w, user, selectedUser.Id, selectedUser.Nick) +} diff --git a/pkg/app/handler/admin/utils.go b/pkg/app/handler/admin/utils.go new file mode 100644 index 0000000..85a25ec --- /dev/null +++ b/pkg/app/handler/admin/utils.go @@ -0,0 +1,112 @@ +// miscellaneous utility functions used for the landing page of the application + +package admin + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderAdminTemplate(w http.ResponseWriter, user *users.User, allUsers []*users.User) { + templates := template.Must( + template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/admin/components/*.tmpl")). + ParseGlob("web/templates/admin/*.tmpl")) + + templates.ExecuteTemplate(w, "view.tmpl", createPageData("admin", user, allUsers, "", "")) +} + +// renderIndexTemplate renders all templates used for the landing page +func renderAdminNewUserTemplate(w http.ResponseWriter, user *users.User, allUsers []*users.User, newUserNick, newUserPass string) { + templates := template.Must( + template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/admin/components/*.tmpl")). + ParseGlob("web/templates/admin/*.tmpl")) + + templates.ExecuteTemplate(w, "view.tmpl", createPageData("admin", user, allUsers, newUserNick, newUserPass)) +} + +// renderIndexTemplate renders all templates used for the landing page +func renderEditUsersTemplate(w http.ResponseWriter, user *users.User, allUsers []*users.User) { + templates := template.Must( + template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/admin/components/*.tmpl")). + ParseGlob("web/templates/admin/edit/*.tmpl")) + + templates.ExecuteTemplate(w, "users.tmpl", createPageData("admin", user, allUsers, "", "")) +} + +// renderIndexTemplate renders all templates used for the landing page +func renderPasswordResetTemplate(w http.ResponseWriter, user *users.User, userId int64, userNick string) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/admin/passwordreset.tmpl")) + + templates.ExecuteTemplate(w, "passwordreset.tmpl", createPasswordResetData("admin", user, userId, userNick)) +} + +// renderIndexTemplate renders all templates used for the landing page +func renderEditPermissionsTemplate(w http.ResponseWriter, user *users.User, allUsers []*users.User) { + templates := template.Must( + template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/admin/components/*.tmpl")). + ParseGlob("web/templates/admin/edit/*.tmpl")) + + templates.ExecuteTemplate(w, "permissions.tmpl", createPageData("admin", user, allUsers, "", "")) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, allUsers []*users.User, newUserNick, newUserPassword string) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + Users []*users.User + NewUserNick string + NewUserPassword string + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + Users: allUsers, + NewUserNick: newUserNick, + NewUserPassword: newUserPassword, + } +} + +// createPageData creates the data used in the template of the landing page +func createPasswordResetData(page string, user *users.User, userId int64, userNick string) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + Users []*users.User + NewUserNick string + NewUserPassword string + UserId int64 + UserNick string + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + UserId: userId, + UserNick: userNick, + } +} diff --git a/pkg/app/handler/all/index.go b/pkg/app/handler/all/index.go new file mode 100644 index 0000000..ef6104a --- /dev/null +++ b/pkg/app/handler/all/index.go @@ -0,0 +1,40 @@ +// Used to show the landing page of the application + +package all + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.View { + authentication.AccessDenied(w, r) + return + } + + var all []*models.Glsa + err := user.CanAccess(connection.DB.Model(&all). + Relation("Bugs"). + Relation("Creator"). + Relation("Comments")). + Select() + + if err != nil { + http.NotFound(w, r) + return + } + + for _, glsa := range all { + glsa.ComputeStatus(user) + } + + renderAllTemplate(w, user, all) +} diff --git a/pkg/app/handler/all/utils.go b/pkg/app/handler/all/utils.go new file mode 100644 index 0000000..a0cfc65 --- /dev/null +++ b/pkg/app/handler/all/utils.go @@ -0,0 +1,36 @@ +// miscellaneous utility functions used for the landing page of the application + +package all + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderAllTemplate(w http.ResponseWriter, user *users.User, all []*models.Glsa) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/all/*.tmpl")) + + templates.ExecuteTemplate(w, "all.tmpl", createPageData("all", user, all)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, all []*models.Glsa) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + All []*models.Glsa + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + All: all, + } +} diff --git a/pkg/app/handler/archive/index.go b/pkg/app/handler/archive/index.go new file mode 100644 index 0000000..61d77dd --- /dev/null +++ b/pkg/app/handler/archive/index.go @@ -0,0 +1,44 @@ +// Used to show the landing page of the application + +package archive + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.View { + authentication.AccessDenied(w, r) + return + } + + var glsas []*models.Glsa + err := user.CanAccess(connection.DB.Model(&glsas). + Where("type = ?", "glsa"). + Relation("Bugs"). + Relation("Creator"). + Relation("Comments")). + Select() + + if err != nil { + logger.Info.Println("Error during glsa selection") + logger.Info.Println(err) + http.NotFound(w, r) + return + } + + for _, glsa := range glsas { + glsa.ComputeStatus(user) + } + + renderArchiveTemplate(w, user, glsas) +} diff --git a/pkg/app/handler/archive/utils.go b/pkg/app/handler/archive/utils.go new file mode 100644 index 0000000..6653691 --- /dev/null +++ b/pkg/app/handler/archive/utils.go @@ -0,0 +1,36 @@ +// miscellaneous utility functions used for the landing page of the application + +package archive + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderArchiveTemplate(w http.ResponseWriter, user *users.User, glsas []*models.Glsa) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/archive/*.tmpl")) + + templates.ExecuteTemplate(w, "archive.tmpl", createPageData("archive", user, glsas)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, glsas []*models.Glsa) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + GLSAs []*models.Glsa + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + GLSAs: glsas, + } +} diff --git a/pkg/app/handler/authentication/accessDenied.go b/pkg/app/handler/authentication/accessDenied.go new file mode 100644 index 0000000..de06ab2 --- /dev/null +++ b/pkg/app/handler/authentication/accessDenied.go @@ -0,0 +1,10 @@ +package authentication + +import ( + "glsamaker/pkg/app/handler/authentication/templates" + "net/http" +) + +func AccessDenied(w http.ResponseWriter, r *http.Request) { + templates.RenderAccessDeniedTemplate(w, r) +} diff --git a/pkg/app/handler/authentication/auth_session/authsession.go b/pkg/app/handler/authentication/auth_session/authsession.go new file mode 100644 index 0000000..c86ca99 --- /dev/null +++ b/pkg/app/handler/authentication/auth_session/authsession.go @@ -0,0 +1,177 @@ +package auth_session + +import ( + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "github.com/google/uuid" + "net/http" + "strings" + "time" +) + +func Create(w http.ResponseWriter, r *http.Request, user *users.User, bindSessionToIP bool, secondFactorMissing bool) { + sessionID := createSessionID() + sessionIP := "*" + expires := time.Now().AddDate(0, 1, 0) + + if bindSessionToIP { + sessionIP = getIP(r) + } + + if secondFactorMissing { + expires = time.Now().Add(10 * time.Minute) + } + + session := &models.Session{ + Id: sessionID, + UserId: user.Id, + IP: sessionIP, + SecondFactorMissing: secondFactorMissing, + Expires: expires, + } + + _, err := connection.DB.Model(session).OnConflict("(id) DO UPDATE").Insert() + if err != nil { + logger.Error.Println("Err during creating session") + logger.Error.Println(err) + } + + createSessionCookie(w, sessionID) +} + +func createSessionID() string { + id, _ := uuid.NewUUID() + return id.String() +} + +func createSessionCookie(w http.ResponseWriter, sessionID string) { + + expires := time.Now().AddDate(0, 1, 0) + + ck := http.Cookie{ + Name: "session", + Domain: "localhost", + Path: "/", + Expires: expires, + } + + ck.Value = sessionID + + http.SetCookie(w, &ck) + +} + +func GetUserId(sessionId, userIP string) int64 { + session := &models.Session{Id: sessionId} + err := connection.DB.Model(session).Relation("User").WherePK().Select() + + if err != nil || session.User.Disabled { + return -1 + } + + if session != nil && + session.Expires.After(time.Now()) && + isValidIP(session.IP, userIP) { + return session.UserId + } else { + return -1 + } +} + +func Only2FAMissing(sessionId, userIP string) bool { + session := &models.Session{Id: sessionId} + err := connection.DB.Model(session).Relation("User").WherePK().Select() + + if err != nil { + return false + } + + invalidateExpiredSession(session) + + return session != nil && + session.Expires.After(time.Now()) && + !session.User.Disabled && + session.SecondFactorMissing && + isValidIP(session.IP, userIP) +} + +func IsLoggedIn(sessionId, userIP string) bool { + + session := &models.Session{Id: sessionId} + err := connection.DB.Model(session).Relation("User").WherePK().Select() + + if err != nil { + return false + } + + invalidateExpiredSession(session) + + return session != nil && + !session.SecondFactorMissing && + !session.User.Disabled && + session.Expires.After(time.Now()) && + isValidIP(session.IP, userIP) +} + +func IsLoggedInAndNeedsNewPassword(sessionId, userIP string) bool { + + session := &models.Session{Id: sessionId} + err := connection.DB.Model(session).Relation("User").WherePK().Select() + + if err != nil { + return false + } + + invalidateExpiredSession(session) + + return session != nil && + !session.SecondFactorMissing && + !session.User.Disabled && + session.User.ForcePasswordRotation && + session.Expires.After(time.Now()) && + isValidIP(session.IP, userIP) +} + +func IsLoggedInAndNeeds2FA(sessionId, userIP string) bool { + + session := &models.Session{Id: sessionId} + err := connection.DB.Model(session).Relation("User").WherePK().Select() + + if err != nil { + return false + } + + invalidateExpiredSession(session) + + return session != nil && + !session.SecondFactorMissing && + !session.User.Disabled && + session.User.Force2FA && + !session.User.IsUsing2FA() && + session.Expires.After(time.Now()) && + isValidIP(session.IP, userIP) +} + +func invalidateExpiredSession(session *models.Session) { + if session.Expires.Before(time.Now()) { + _, err := connection.DB.Model(session).WherePK().Delete() + if err != nil { + logger.Error.Println("Error deleting expired session.") + logger.Error.Println(err) + } + } +} + +func isValidIP(sessionIP, userIP string) bool { + return sessionIP == "*" || userIP == sessionIP +} + +func getIP(r *http.Request) string { + forwarded := r.Header.Get("X-FORWARDED-FOR") + if forwarded != "" { + return strings.Split(forwarded, ":")[0] + } + return strings.Split(r.RemoteAddr, ":")[0] +} diff --git a/pkg/app/handler/authentication/login.go b/pkg/app/handler/authentication/login.go new file mode 100644 index 0000000..7cd5c87 --- /dev/null +++ b/pkg/app/handler/authentication/login.go @@ -0,0 +1,100 @@ +package authentication + +import ( + "glsamaker/pkg/app/handler/authentication/auth_session" + "glsamaker/pkg/app/handler/authentication/templates" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models/users" + "golang.org/x/crypto/argon2" + "net/http" +) + +func Login(w http.ResponseWriter, r *http.Request) { + + // in case '/login' is request but the user is + // already authenticated we will redirect to '/' + if utils.IsAuthenticated(w, r) { + http.Redirect(w, r, "/", 301) + } + + username, pass, cameFrom, bindLoginToIP, _ := getParams(r) + + if IsValidPassword(username, pass) { + user, _ := getLoginUser(username) + auth_session.Create(w, r, user, bindLoginToIP, user.IsUsing2FA()) + if user.IsUsing2FA() { + http.Redirect(w, r, "/login/2fa", 301) + } else { + http.Redirect(w, r, cameFrom, 301) + } + } else { + templates.RenderLoginTemplate(w, r) + } + +} + +func SecondFactorLogin(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + + if user == nil || !user.IsUsing2FA() { + // this should not occur + http.NotFound(w, r) + return + } + + if user.IsUsingTOTP { + templates.RenderTOTPTemplate(w, r) + } else if user.IsUsingWebAuthn { + templates.RenderWebAuthnTemplate(w, r) + } else { + // this should not occur + http.NotFound(w, r) + } +} + +// utility functions + +func getLoginUser(username string) (*users.User, bool) { + var potenialUsers []*users.User + err := connection.DB.Model(&potenialUsers).Where("nick = ?", username).Select() + isValidUser := err == nil + + if len(potenialUsers) < 1 { + return &users.User{}, false + } + + return potenialUsers[0], isValidUser +} + +func getParams(r *http.Request) (string, string, string, bool, error) { + err := r.ParseForm() + if err != nil { + return "", "", "", false, err + } + username := r.Form.Get("username") + password := r.Form.Get("password") + cameFrom := r.Form.Get("cameFrom") + restrictLogin := r.Form.Get("restrictlogin") + return username, password, cameFrom, restrictLogin == "on", err +} + +func IsValidPassword(username string, password string) bool { + user, isValidUser := getLoginUser(username) + if !isValidUser { + return false + } + + hashedPassword := argon2.IDKey( + []byte(password), + user.Password.Salt, + user.Password.Time, + user.Password.Memory, + user.Password.Threads, + user.Password.KeyLen) + + if user != nil && !user.Disabled && string(user.Password.Hash) == string(hashedPassword) { + return true + } + return false +} diff --git a/pkg/app/handler/authentication/logout.go b/pkg/app/handler/authentication/logout.go new file mode 100644 index 0000000..87d17f4 --- /dev/null +++ b/pkg/app/handler/authentication/logout.go @@ -0,0 +1,29 @@ +package authentication + +import ( + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "net/http" +) + +func Logout(w http.ResponseWriter, r *http.Request) { + + sessionID, err := r.Cookie("session") + + if err != nil || sessionID == nil { + // TODO Error + } + + session := &models.Session{Id: sessionID.Value} + _, err = connection.DB.Model(session).WherePK().Delete() + + if err != nil { + logger.Info.Println("Error deleting session") + logger.Error.Println("Error deleting session") + logger.Error.Println(err) + } + + http.Redirect(w, r, "/", 301) + +} diff --git a/pkg/app/handler/authentication/templates/admin.go b/pkg/app/handler/authentication/templates/admin.go new file mode 100644 index 0000000..12d039e --- /dev/null +++ b/pkg/app/handler/authentication/templates/admin.go @@ -0,0 +1,36 @@ +package templates + +import ( + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the login page +func RenderAccessDeniedTemplate(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/authentication/accessDenied.tmpl")) + + templates.ExecuteTemplate(w, "accessDenied.tmpl", createAccessDeniedData(user)) +} + +// createPageData creates the data used in the template of the landing page +func createAccessDeniedData(user *users.User) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + }{ + Page: "", + Application: models.GetDefaultGlobalSettings(), + User: user, + } +} diff --git a/pkg/app/handler/authentication/templates/login.go b/pkg/app/handler/authentication/templates/login.go new file mode 100644 index 0000000..2e3b241 --- /dev/null +++ b/pkg/app/handler/authentication/templates/login.go @@ -0,0 +1,32 @@ +package templates + +import ( + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the login page +func RenderLoginTemplate(w http.ResponseWriter, r *http.Request) { + + data := struct { + CameFrom string + }{ + CameFrom: getPath(r), + } + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/authentication/login.tmpl")) + + templates.ExecuteTemplate(w, "login.tmpl", data) +} + +func getPath(r *http.Request) string { + if r.URL.RawQuery == "" { + return r.URL.Path + } else { + return r.URL.Path + "?" + r.URL.RawQuery + } +} diff --git a/pkg/app/handler/authentication/templates/totp.go b/pkg/app/handler/authentication/templates/totp.go new file mode 100644 index 0000000..acb34e5 --- /dev/null +++ b/pkg/app/handler/authentication/templates/totp.go @@ -0,0 +1,23 @@ +package templates + +import ( + "html/template" + "net/http" +) + +func RenderTOTPTemplate(w http.ResponseWriter, r *http.Request) { + + data := struct { + CameFrom string + }{ + CameFrom: getPath(r), + } + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/authentication/totp.tmpl")) + + templates.ExecuteTemplate(w, "totp.tmpl", data) +} diff --git a/pkg/app/handler/authentication/templates/webauthn.go b/pkg/app/handler/authentication/templates/webauthn.go new file mode 100644 index 0000000..148f475 --- /dev/null +++ b/pkg/app/handler/authentication/templates/webauthn.go @@ -0,0 +1,23 @@ +package templates + +import ( + "html/template" + "net/http" +) + +func RenderWebAuthnTemplate(w http.ResponseWriter, r *http.Request) { + + data := struct { + CameFrom string + }{ + CameFrom: getPath(r), + } + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/authentication/webauthn.tmpl")) + + templates.ExecuteTemplate(w, "webauthn.tmpl", data) +} diff --git a/pkg/app/handler/authentication/totp/totp.go b/pkg/app/handler/authentication/totp/totp.go new file mode 100644 index 0000000..00e6b83 --- /dev/null +++ b/pkg/app/handler/authentication/totp/totp.go @@ -0,0 +1,60 @@ +package totp + +import ( + "glsamaker/pkg/app/handler/authentication/auth_session" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/models/users" + "bytes" + "encoding/base64" + "github.com/pquerna/otp/totp" + "image/png" + "net/http" + "time" +) + +func Login(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + token, err := getParam(r) + + if user == nil || err != nil || !IsValidTOTPToken(user, token) { + http.Redirect(w, r, "/login/2fa", 301) + } else { + auth_session.Create(w, r, user, true, false) + http.Redirect(w, r, "/", 301) + } + +} + +func IsValidTOTPToken(user *users.User, token string) bool { + return totp.Validate(token, user.TOTPSecret) +} + +func GetToken(user *users.User) string { + token, _ := totp.GenerateCode(user.TOTPSecret, time.Now()) + return token +} + +func Generate(email string) (string, string) { + + key, _ := totp.Generate(totp.GenerateOpts{ + Issuer: "glsamakertest.gentoo.org", + AccountName: email, + }) + + var buf bytes.Buffer + img, _ := key.Image(250, 250) + + png.Encode(&buf, img) + + return key.Secret(), base64.StdEncoding.EncodeToString(buf.Bytes()) +} + +func getParam(r *http.Request) (string, error) { + err := r.ParseForm() + if err != nil { + return "", err + } + token := r.Form.Get("token") + return token, err +} diff --git a/pkg/app/handler/authentication/utils/utils.go b/pkg/app/handler/authentication/utils/utils.go new file mode 100644 index 0000000..d06a2d7 --- /dev/null +++ b/pkg/app/handler/authentication/utils/utils.go @@ -0,0 +1,81 @@ +package utils + +import ( + "glsamaker/pkg/app/handler/authentication/auth_session" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models/users" + "net/http" + "strings" +) + +// utility methods to check whether a user is authenticated + +func Only2FAMissing(w http.ResponseWriter, r *http.Request) bool { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + return err == nil && sessionID != nil && auth_session.Only2FAMissing(sessionID.Value, userIP) +} + +func IsAuthenticated(w http.ResponseWriter, r *http.Request) bool { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + return err == nil && sessionID != nil && auth_session.IsLoggedIn(sessionID.Value, userIP) +} + +func IsAuthenticatedAndNeedsNewPassword(w http.ResponseWriter, r *http.Request) bool { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + return err == nil && sessionID != nil && auth_session.IsLoggedInAndNeedsNewPassword(sessionID.Value, userIP) +} + +func IsAuthenticatedAndNeeds2FA(w http.ResponseWriter, r *http.Request) bool { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + return err == nil && sessionID != nil && auth_session.IsLoggedInAndNeeds2FA(sessionID.Value, userIP) +} + +func IsAuthenticatedAsAdmin(w http.ResponseWriter, r *http.Request) bool { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + if err != nil || sessionID == nil || !auth_session.IsLoggedIn(sessionID.Value, userIP) { + return false + } + + user := GetAuthenticatedUser(r) + + return user != nil && user.Permissions.Admin.View + +} + +func GetAuthenticatedUser(r *http.Request) *users.User { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + if err != nil || sessionID == nil || !(auth_session.IsLoggedIn(sessionID.Value, userIP) || auth_session.Only2FAMissing(sessionID.Value, userIP)) { + return nil + } + + userId := auth_session.GetUserId(sessionID.Value, userIP) + + user := &users.User{Id: userId} + err = connection.DB.Select(user) + + if err != nil { + return nil + } + + return user +} + +func getIP(r *http.Request) string { + forwarded := r.Header.Get("X-FORWARDED-FOR") + if forwarded != "" { + return strings.Split(forwarded, ":")[0] + } + return strings.Split(r.RemoteAddr, ":")[0] +} diff --git a/pkg/app/handler/authentication/webauthn/login.go b/pkg/app/handler/authentication/webauthn/login.go new file mode 100644 index 0000000..7bf9c1d --- /dev/null +++ b/pkg/app/handler/authentication/webauthn/login.go @@ -0,0 +1,118 @@ +package webauthn + +import ( + "glsamaker/pkg/app/handler/authentication/auth_session" + "glsamaker/pkg/app/handler/authentication/utils" + "encoding/json" + "fmt" + "github.com/duo-labs/webauthn.io/session" + webauthn_lib "github.com/duo-labs/webauthn/webauthn" + "log" + "net/http" +) + +var ( + WebAuthn *webauthn_lib.WebAuthn + SessionStore *session.Store +) + + +func BeginLogin(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + CreateWebAuthn() + CreateSessionStore() + + // user doesn't exist + if user == nil { + log.Println("Error fetching the user.") + JsonResponse(w, "Error fetching the user.", http.StatusBadRequest) + return + } + + // generate PublicKeyCredentialRequestOptions, session data + options, sessionData, err := WebAuthn.BeginLogin(user) + if err != nil { + log.Println(err) + JsonResponse(w, err.Error(), http.StatusInternalServerError) + return + } + + // store session data as marshaled JSON + err = SessionStore.SaveWebauthnSession("authentication", sessionData, r, w) + if err != nil { + log.Println(err) + JsonResponse(w, err.Error(), http.StatusInternalServerError) + return + } + + JsonResponse(w, options, http.StatusOK) +} + +func FinishLogin(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + // user doesn't exist + if user == nil { + log.Println("Error fetching the user.") + JsonResponse(w, "Error fetching the user.", http.StatusBadRequest) + return + } + + // load the session data + sessionData, err := SessionStore.GetWebauthnSession("authentication", r) + if err != nil { + log.Println(err) + JsonResponse(w, err.Error(), http.StatusBadRequest) + return + } + + // in an actual implementation, we should perform additional checks on + // the returned 'credential', i.e. check 'credential.Authenticator.CloneWarning' + // and then increment the credentials counter + _, err = WebAuthn.FinishLogin(user, sessionData, r) + if err != nil { + log.Println(err) + JsonResponse(w, err.Error(), http.StatusBadRequest) + return + } + + // handle successful login + // TODO handle bindLoginToIP correctly + auth_session.Create(w, r, user, true, false) + JsonResponse(w, "Login Success", http.StatusOK) +} + +// from: https://github.com/duo-labs/webauthn.io/blob/3f03b482d21476f6b9fb82b2bf1458ff61a61d41/server/response.go#L15 +func JsonResponse(w http.ResponseWriter, d interface{}, c int) { + dj, err := json.Marshal(d) + if err != nil { + http.Error(w, "Error creating JSON response", http.StatusInternalServerError) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(c) + fmt.Fprintf(w, "%s", dj) +} + +func CreateWebAuthn() { + + if WebAuthn == nil { + authn, _ := webauthn_lib.New(&webauthn_lib.Config{ + RPDisplayName: "Gentoo GLSAMaker", // Display Name for your site + RPID: "glsamakertest.gentoo.org", // Generally the domain name for your site + RPOrigin: "https://glsamakertest.gentoo.org", // The origin URL for WebAuthn requests + RPIcon: "https://assets.gentoo.org/tyrian/site-logo.png", // Optional icon URL for your site + }) + + WebAuthn = authn + } + +} + +func CreateSessionStore() { + if SessionStore == nil { + SessionStore, _ = session.NewStore() + } +} diff --git a/pkg/app/handler/authentication/webauthn/register.go b/pkg/app/handler/authentication/webauthn/register.go new file mode 100644 index 0000000..4e299b3 --- /dev/null +++ b/pkg/app/handler/authentication/webauthn/register.go @@ -0,0 +1,111 @@ +package webauthn + +import ( + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "fmt" + "github.com/duo-labs/webauthn/protocol" + "log" + "net/http" +) + +func BeginRegistration(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + + CreateWebAuthn() + CreateSessionStore() + + if user == nil { + JsonResponse(w, fmt.Errorf("must supply a valid username i.e. foo@bar.com"), http.StatusBadRequest) + return + } + + registerOptions := func(credCreationOpts *protocol.PublicKeyCredentialCreationOptions) { + credCreationOpts.CredentialExcludeList = user.CredentialExcludeList() + } + + // generate PublicKeyCredentialCreationOptions, session data + //var options *protocol.CredentialCreation + //var err error + options, sessionData, err := WebAuthn.BeginRegistration( + user, + registerOptions, + ) + + if err != nil { + log.Println("Error begin register") + log.Println(err) + JsonResponse(w, err.Error(), http.StatusInternalServerError) + return + } + + // store session data as marshaled JSON + err = SessionStore.SaveWebauthnSession("registration", sessionData, r, w) + if err != nil { + log.Println("Error store session") + log.Println(err) + JsonResponse(w, err.Error(), http.StatusInternalServerError) + return + } + + JsonResponse(w, options, http.StatusOK) +} + +func FinishRegistration(w http.ResponseWriter, r *http.Request) { + + authname := getParams(r) + user := utils.GetAuthenticatedUser(r) + + if user == nil { + JsonResponse(w, "Cannot find User", http.StatusBadRequest) + return + } + + // load the session data + sessionData, err := SessionStore.GetWebauthnSession("registration", r) + if err != nil { + log.Println("Error loading session") + log.Println(err) + JsonResponse(w, err.Error(), http.StatusBadRequest) + return + } + + credential, err := WebAuthn.FinishRegistration(user, sessionData, r) + if err != nil { + log.Println("Error finish session") + log.Println(err) + JsonResponse(w, err.Error(), http.StatusBadRequest) + return + } + + user.AddCredential(*credential, authname) + + _, err = connection.DB.Model(user).Column("webauthn_credentials").WherePK().Update() + _, err = connection.DB.Model(user).Column("webauthn_credential_names").WherePK().Update() + if err != nil { + logger.Error.Println("Error adding WebAuthn credentials.") + logger.Error.Println(err) + } + + JsonResponse(w, "Registration Success", http.StatusOK) +} + +func getParams(r *http.Request) string { + + keys, ok := r.URL.Query()["name"] + + if !ok || len(keys[0]) < 1 { + logger.Info.Println("Url Param 'name' is missing") + return "Unnamed Authenticator" + } + + // we only want the single item. + key := keys[0] + + if len(key) > 20 { + key = key[0:20] + } + + return key +} diff --git a/pkg/app/handler/cvetool/bug.go b/pkg/app/handler/cvetool/bug.go new file mode 100644 index 0000000..7725c88 --- /dev/null +++ b/pkg/app/handler/cvetool/bug.go @@ -0,0 +1,85 @@ +package cvetool + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models/bugzilla" + "glsamaker/pkg/models/cve" + "net/http" +) + +// Show renders a template to show the landing page of the application +func AssignBug(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.CVETool.AssignBug { + authentication.AccessDenied(w, r) + return + } + + cveId, bugId, err := getBugAssignParams(r) + + // TODO validate bug using bugzilla api before continue + + cveItem := &cve.DefCveItem{Id: cveId} + err = connection.DB.Select(cveItem) + + if err != nil { + w.Write([]byte("err")) + return + } + + cveItem.State = "Assigned" + + logger.Info.Println("bugId") + logger.Info.Println(bugId) + + //assign bug + newBugs := bugzilla.GetBugsByIds([]string{bugId}) + + for _, newBug := range newBugs { + _, err = connection.DB.Model(&newBug).OnConflict("(id) DO UPDATE").Insert() + + if err != nil { + logger.Info.Println("Error creating bug") + logger.Info.Println(err) + } + + cveToBug := &cve.DefCveItemToBug{ + DefCveItemId: cveId, + BugId: newBug.Id, + } + + connection.DB.Model(cveToBug).Insert() + + } + + // TODO MIGRATION + //cveItem.Bugs = append(cveItem.Bugs, bugId) + + _, err = connection.DB.Model(cveItem).Column("bugs").WherePK().Update() + _, err = connection.DB.Model(cveItem).Column("state").WherePK().Update() + + if err != nil { + logger.Info.Println("Err") + logger.Info.Println(err) + w.Write([]byte("err")) + return + } + + w.Write([]byte("ok")) + +} + +func getBugAssignParams(r *http.Request) (string, string, error) { + err := r.ParseForm() + if err != nil { + return "", "", err + } + cveid := r.Form.Get("cveid") + bugid := r.Form.Get("bugid") + return cveid, bugid, err +} diff --git a/pkg/app/handler/cvetool/comments.go b/pkg/app/handler/cvetool/comments.go new file mode 100644 index 0000000..d36122a --- /dev/null +++ b/pkg/app/handler/cvetool/comments.go @@ -0,0 +1,74 @@ +package cvetool + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models/cve" + "encoding/json" + "net/http" + "time" +) + +// Show renders a template to show the landing page of the application +func AddComment(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.CVETool.Comment { + authentication.AccessDenied(w, r) + return + } + + id, comment, err := getParams(r) + + newComment, err := addNewCommment(id, user.Id, comment) + + if err != nil { + logger.Info.Println("Err") + logger.Info.Println(err) + w.Write([]byte("err")) + return + } + + newCommentString, _ := json.Marshal(newComment) + + w.Write(newCommentString) + +} + +func addNewCommment(id string, userID int64, comment string) (cve.Comment, error) { + + cveItem := &cve.DefCveItem{Id: id} + err := connection.DB.Select(cveItem) + + if err != nil { + return cve.Comment{}, err + } + + newComment := cve.Comment{ + CVEId: id, + User: userID, + Message: comment, + Date: time.Now(), + } + + //cveItem.Comments = append(cveItem.Comments, newComment) + + //_, err = connection.DB.Model(cveItem).Column("comments").WherePK().Update() + _, err = connection.DB.Model(&newComment).Insert() + + return newComment, err + +} + +func getParams(r *http.Request) (string, string, error) { + err := r.ParseForm() + if err != nil { + return "", "", err + } + id := r.Form.Get("cveid") + comment := r.Form.Get("comment") + return id, comment, err +} diff --git a/pkg/app/handler/cvetool/index.go b/pkg/app/handler/cvetool/index.go new file mode 100644 index 0000000..9c54a01 --- /dev/null +++ b/pkg/app/handler/cvetool/index.go @@ -0,0 +1,169 @@ +// Used to show the landing page of the application + +package cvetool + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models/cve" + "encoding/json" + "fmt" + "github.com/go-pg/pg/v9/orm" + "net/http" + "strconv" + "strings" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.CVETool.View { + authentication.AccessDenied(w, r) + return + } + + renderIndexTemplate(w, user) +} + +// Show renders a template to show the landing page of the application +func ShowFullscreen(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.CVETool.View { + authentication.AccessDenied(w, r) + return + } + + renderIndexFullscreenTemplate(w, user) +} + +// Show renders a template to show the landing page of the application +func Add(w http.ResponseWriter, r *http.Request) { + //renderIndexTemplate(w) +} + +// Show renders a template to show the landing page of the application +func CveData(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.CVETool.View { + authentication.AccessDenied(w, r) + return + } + + type DataTableData struct { + Draw int `json:"draw"` + RecordsTotal int `json:"recordsTotal"` + RecordsFiltered int `json:"recordsFiltered"` + Data [][]string `json:"data"` + } + + draw, _ := strconv.Atoi(getParam(r, "draw")) + start, _ := strconv.Atoi(getParam(r, "start")) + length, _ := strconv.Atoi(getParam(r, "length")) + order_column := getParam(r, "order[0][column]") + order_dir := strings.ToUpper(getParam(r, "order[0][dir]")) + search_value := strings.ToUpper(getParam(r, "search[value]")) + + state_value := getParam(r, "columns[10][search][value]") + logger.Info.Println("state_value") + logger.Info.Println(state_value) + + count_overall, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Count() + count, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state LIKE " + "'%" + state_value + "%'").WhereGroup(func(q *orm.Query) (*orm.Query, error) { + q = q.WhereOr("description LIKE " + "'%" + search_value + "%'"). + WhereOr("id LIKE " + "'%" + search_value + "%'") + return q, nil + }).Count() + + order := "id" + if order_column == "0" { + order = "id" + } else if order_column == "8" { + order = "last_modified_date" + } else if order_column == "9" { + order = "published_date" + } else if order_column == "10" { + order = "state" + } + + var dataTableEntries [][]string + var cves []*cve.DefCveItem + err := connection.DB.Model(&cves).Order(order + " " + order_dir).Offset(start).Limit(length).Where("state LIKE " + "'%" + state_value + "%'").WhereGroup(func(q *orm.Query) (*orm.Query, error) { + q = q.WhereOr("description LIKE " + "'%" + search_value + "%'"). + WhereOr("id LIKE " + "'%" + search_value + "%'") + return q, nil + }).Relation("Bugs").Relation("Comments").Select() + + if err != nil || len(cves) == 0 { + logger.Info.Println("Error finding cves:") + logger.Info.Println(err) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"draw":` + strconv.Itoa(draw) + `,"recordsTotal":` + strconv.Itoa(count_overall) + `,"recordsFiltered":0,"data":[]}`)) + return + } else { + for _, cve := range cves { + + // TODO handle empty + + baseScore := "" + impact := "" + if cve.Impact != nil { + baseScore = fmt.Sprintf("%.2f", cve.Impact.BaseMetricV3.CvssV3.BaseScore) + impact = cve.Impact.BaseMetricV3.CvssV3.VectorString + } + + var referenceList []string + for _, reference := range cve.Cve.References.ReferenceData { + referenceList = append(referenceList, "<a href=\""+reference.Url+"\">source</a>") + //referenceList = append(referenceList, "<a href=\"" + reference.Url + "\">" + strings.ToLower(reference.Refsource) + "</a>") + } + references := strings.Join(referenceList, ", ") + + comments, _ := json.Marshal(cve.Comments) + + packages, _ := json.Marshal(cve.Packages) + bugs, _ := json.Marshal(cve.Bugs) + + dataTableEntries = append(dataTableEntries, []string{ + cve.Id, + cve.Description, + string(packages), // TODO MIGRATION strings.Join(cve.Packages, ","), + string(bugs), // TODO MIGRATION strings.Join(cve.Bugs, ","), + baseScore, + impact, + references, + string(comments), + cve.LastModifiedDate, + cve.PublishedDate, + cve.State, + "changelog"}) + } + } + + dataTableData := DataTableData{ + Draw: draw, + RecordsTotal: count_overall, + RecordsFiltered: count, + Data: dataTableEntries, + } + + res, _ := json.Marshal(dataTableData) + + w.Header().Set("Content-Type", "application/json") + w.Write(res) +} + +func getParam(r *http.Request, keyname string) string { + keys, ok := r.URL.Query()[keyname] + if !ok || len(keys[0]) < 1 { + return "" + } + result := keys[0] + return result +} diff --git a/pkg/app/handler/cvetool/state.go b/pkg/app/handler/cvetool/state.go new file mode 100644 index 0000000..608691c --- /dev/null +++ b/pkg/app/handler/cvetool/state.go @@ -0,0 +1,75 @@ +package cvetool + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models/cve" + "encoding/json" + "net/http" +) + +// Show renders a template to show the landing page of the application +func ChangeState(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.CVETool.ChangeState { + authentication.AccessDenied(w, r) + return + } + + if !user.CanEditCVEs() { + logger.Error.Println("Err, user can not edit.") + w.Write([]byte("err")) + return + } + + id, newState, reason, err := getStateParams(r) + + cveItem := &cve.DefCveItem{Id: id} + err = connection.DB.Select(cveItem) + + if err != nil || reason == "" || cveItem.State == "Assigned" || !(newState == "NFU" || newState == "Later" || newState == "Invalid") { + logger.Error.Println("Err, invalid data") + logger.Error.Println(err) + w.Write([]byte("err")) + return + } + + cveItem.State = newState + _, err = connection.DB.Model(cveItem).Column("state").WherePK().Update() + + if err != nil { + logger.Error.Println("Err") + logger.Error.Println(err) + w.Write([]byte("err")) + return + } + + newComment, err := addNewCommment(id, user.Id, "Changed status to "+newState+": "+reason) + + if err != nil { + logger.Error.Println("Err") + logger.Error.Println(err) + w.Write([]byte("err")) + return + } + + newCommentString, _ := json.Marshal(newComment) + + w.Write(newCommentString) + +} + +func getStateParams(r *http.Request) (string, string, string, error) { + err := r.ParseForm() + if err != nil { + return "", "", "", err + } + id := r.Form.Get("cveid") + newstate := r.Form.Get("newstate") + reason := r.Form.Get("reason") + return id, newstate, reason, err +} diff --git a/pkg/app/handler/cvetool/update.go b/pkg/app/handler/cvetool/update.go new file mode 100644 index 0000000..8ce12f5 --- /dev/null +++ b/pkg/app/handler/cvetool/update.go @@ -0,0 +1,23 @@ +package cvetool + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/cveimport" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Update(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.CVETool.UpdateCVEs { + authentication.AccessDenied(w, r) + return + } + + go cveimport.IncrementalCVEImport() + + http.Redirect(w, r, "/", 301) +} diff --git a/pkg/app/handler/cvetool/utils.go b/pkg/app/handler/cvetool/utils.go new file mode 100644 index 0000000..7e78660 --- /dev/null +++ b/pkg/app/handler/cvetool/utils.go @@ -0,0 +1,47 @@ +// miscellaneous utility functions used for the landing page of the application + +package cvetool + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderIndexTemplate(w http.ResponseWriter, user *users.User) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/index/*.tmpl")) + + templates.ExecuteTemplate(w, "show.tmpl", createPageData("cvetool", user)) +} + +// renderIndexTemplate renders all templates used for the landing page +func renderIndexFullscreenTemplate(w http.ResponseWriter, user *users.User) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/index/*.tmpl")) + + templates.ExecuteTemplate(w, "showFullscreen.tmpl", createPageData("cvetool", user)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + CanEdit bool + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + CanEdit: user.CanEditCVEs(), + } +} diff --git a/pkg/app/handler/dashboard/index.go b/pkg/app/handler/dashboard/index.go new file mode 100644 index 0000000..82fd858 --- /dev/null +++ b/pkg/app/handler/dashboard/index.go @@ -0,0 +1,58 @@ +// Used to show the landing page of the application + +package dashboard + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/app/handler/statistics" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models" + "glsamaker/pkg/models/cve" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !(user.Permissions.Glsa.View && user.Permissions.CVETool.View) { + authentication.AccessDenied(w, r) + return + } + + var glsas []*models.Glsa + user.CanAccess(connection.DB.Model(&glsas).Relation("Creator").Order("updated DESC").Limit(5)).Select() + + var cves []*cve.DefCveItem + connection.DB.Model(&cves).Order("last_modified_date DESC").Limit(5).Select() + + var comments []*cve.Comment + connection.DB.Model(&comments).Order("date DESC").Limit(5).Select() + + requests, _ := connection.DB.Model((*models.Glsa)(nil)).Where("type = ?", "request").Count() + drafts, _ := connection.DB.Model((*models.Glsa)(nil)).Where("type = ?", "draft").Count() + glsasCount, _ := connection.DB.Model((*models.Glsa)(nil)).Where("type = ?", "glsa").Count() + allGlsas, _ := connection.DB.Model((*models.Glsa)(nil)).Count() + + new, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "New").Count() + assigned, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "Assigned").Count() + nfu, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "NFU").Count() + later, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "Later").Count() + invalid, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "Invalid").Count() + allCVEs, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Count() + + statisticsData := statistics.StatisticsData{ + Requests: float64(requests) / float64(allGlsas), + Drafts: float64(drafts) / float64(allGlsas), + Glsas: float64(glsasCount) / float64(allGlsas), + New: float64(new) / float64(allCVEs), + Assigned: float64(assigned) / float64(allCVEs), + NFU: float64(nfu) / float64(allCVEs), + Later: float64(later) / float64(allCVEs), + Invalid: float64(invalid) / float64(allCVEs), + } + + renderDashboardTemplate(w, user, glsas, cves, comments, &statisticsData) +} diff --git a/pkg/app/handler/dashboard/utils.go b/pkg/app/handler/dashboard/utils.go new file mode 100644 index 0000000..ed0bc91 --- /dev/null +++ b/pkg/app/handler/dashboard/utils.go @@ -0,0 +1,44 @@ +// miscellaneous utility functions used for the landing page of the application + +package dashboard + +import ( + "glsamaker/pkg/app/handler/statistics" + "glsamaker/pkg/models" + "glsamaker/pkg/models/cve" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderDashboardTemplate(w http.ResponseWriter, user *users.User, glsas []*models.Glsa, cves []*cve.DefCveItem, comments []*cve.Comment, statisticsData *statistics.StatisticsData) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/dashboard/*.tmpl")) + + templates.ExecuteTemplate(w, "dashboard.tmpl", createPageData("dashboard", user, glsas, cves, comments, statisticsData)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, glsas []*models.Glsa, cves []*cve.DefCveItem, comments []*cve.Comment, statisticsData *statistics.StatisticsData) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + GLSAs []*models.Glsa + CVEs []*cve.DefCveItem + Comments []*cve.Comment + StatisticsData *statistics.StatisticsData + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + GLSAs: glsas, + CVEs: cves, + Comments: comments, + StatisticsData: statisticsData, + } +} diff --git a/pkg/app/handler/drafts/index.go b/pkg/app/handler/drafts/index.go new file mode 100644 index 0000000..cfc699f --- /dev/null +++ b/pkg/app/handler/drafts/index.go @@ -0,0 +1,44 @@ +// Used to show the landing page of the application + +package drafts + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.View { + authentication.AccessDenied(w, r) + return + } + + var drafts []*models.Glsa + err := user.CanAccess(connection.DB.Model(&drafts). + Where("type = ?", "draft"). + Relation("Bugs"). + Relation("Creator"). + Relation("Comments")). + Select() + + if err != nil { + logger.Info.Println("Error during draft selection") + logger.Info.Println(err) + http.NotFound(w, r) + return + } + + for _, draft := range drafts { + draft.ComputeStatus(user) + } + + renderDraftsTemplate(w, user, drafts) +} diff --git a/pkg/app/handler/drafts/utils.go b/pkg/app/handler/drafts/utils.go new file mode 100644 index 0000000..f7f4f57 --- /dev/null +++ b/pkg/app/handler/drafts/utils.go @@ -0,0 +1,37 @@ +// miscellaneous utility functions used for the landing page of the application + +package drafts + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderDraftsTemplate(w http.ResponseWriter, user *users.User, drafts []*models.Glsa) { + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/drafts/*.tmpl")) + + templates.ExecuteTemplate(w, "drafts.tmpl", createPageData("drafts", user, drafts)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, drafts []*models.Glsa) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + Drafts []*models.Glsa + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + Drafts: drafts, + } +} diff --git a/pkg/app/handler/glsa/bugs.go b/pkg/app/handler/glsa/bugs.go new file mode 100644 index 0000000..9b3d32e --- /dev/null +++ b/pkg/app/handler/glsa/bugs.go @@ -0,0 +1,63 @@ +package glsa + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/bugzilla" + "net/http" + "strconv" +) + +// Show renders a template to show the landing page of the application +func UpdateBugs(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.UpdateBugs { + authentication.AccessDenied(w, r) + return + } + + go bugUpdate() + + http.Redirect(w, r, "/", 301) +} + +func bugUpdate() { + + var allBugs []*bugzilla.Bug + connection.DB.Model(&allBugs).Select() + + var bugIdsLists [][]string + bugIdsLists = append(bugIdsLists, []string{}) + for _, bug := range allBugs { + lastElem := bugIdsLists[len(bugIdsLists)-1] + + if len(lastElem) < 100 { + bugIdsLists[len(bugIdsLists)-1] = append(lastElem, strconv.FormatInt(bug.Id, 10)) + } else { + bugIdsLists = append(bugIdsLists, []string{strconv.FormatInt(bug.Id, 10)}) + } + } + + for _, bugIdsList := range bugIdsLists { + updatedBugs := bugzilla.GetBugsByIds(bugIdsList) + + for _, updatedBug := range updatedBugs { + _, err := connection.DB.Model(&updatedBug).WherePK().Update() + if err != nil { + logger.Error.Println("Error during bug data update") + logger.Error.Println(err) + } + } + } + + // Possibly delete deleted bugs + // Do we even delete bugs? + + // update the time of the last bug update + models.SetApplicationValue("LastBugUpdate", "") +} diff --git a/pkg/app/handler/glsa/comments.go b/pkg/app/handler/glsa/comments.go new file mode 100644 index 0000000..9412b62 --- /dev/null +++ b/pkg/app/handler/glsa/comments.go @@ -0,0 +1,110 @@ +package glsa + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/cve" + "glsamaker/pkg/models/users" + "encoding/json" + "errors" + "net/http" + "strconv" + "time" +) + +// Show renders a template to show the landing page of the application +func AddComment(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.Comment { + authentication.AccessDenied(w, r) + return + } + + if !user.CanEditCVEs() { + w.Write([]byte("err")) + return + } + + id, comment, commentType, err := getParams(r) + + newComment, err := AddNewCommment(id, user, comment, commentType) + + if err != nil { + logger.Info.Println("Err") + logger.Info.Println(err) + w.Write([]byte("err")) + return + } + + newCommentString, _ := json.Marshal(newComment) + + w.Write(newCommentString) + +} + +func AddNewCommment(id string, user *users.User, comment string, commentType string) (cve.Comment, error) { + + glsaID, err := strconv.ParseInt(id, 10, 64) + + if err != nil { + return cve.Comment{}, err + } + + glsa := &models.Glsa{Id: glsaID} + err = user.CanAccess(connection.DB.Model(glsa).WherePK()).Select() + + if err != nil { + return cve.Comment{}, err + } + + // TODO: VALIDATE !! + + if commentType == "approve" && !user.Permissions.Glsa.Approve { + return cve.Comment{}, errors.New("ACCESS DENIED") + } else if commentType == "approve" && glsa.CreatorId == user.Id && !user.Permissions.Glsa.ApproveOwnGlsa { + return cve.Comment{}, errors.New("ACCESS DENIED") + } else if commentType == "decline" && !user.Permissions.Glsa.Decline { + return cve.Comment{}, errors.New("ACCESS DENIED") + } + + if commentType == "approve" { + glsa.ApprovedBy = append(glsa.ApprovedBy, user.Id) + _, err = connection.DB.Model(glsa).Column("approved_by").WherePK().Update() + } else if commentType == "decline" { + glsa.DeclinedBy = append(glsa.DeclinedBy, user.Id) + _, err = connection.DB.Model(glsa).Column("declined_by").WherePK().Update() + } + + newComment := cve.Comment{ + GlsaId: glsaID, + User: user.Id, + UserBadge: user.Badge, + Type: commentType, + Message: comment, + Date: time.Now(), + } + + glsa.Comments = append(glsa.Comments, newComment) + + //_, err = connection.DB.Model(glsa).Column("comments").WherePK().Update() + _, err = connection.DB.Model(&newComment).Insert() + + return newComment, err + +} + +func getParams(r *http.Request) (string, string, string, error) { + err := r.ParseForm() + if err != nil { + return "", "", "", err + } + id := r.Form.Get("glsaid") + comment := r.Form.Get("comment") + commentType := r.Form.Get("commentType") + return id, comment, commentType, err +} diff --git a/pkg/app/handler/glsa/delete.go b/pkg/app/handler/glsa/delete.go new file mode 100644 index 0000000..b036319 --- /dev/null +++ b/pkg/app/handler/glsa/delete.go @@ -0,0 +1,42 @@ +// Used to show the landing page of the application + +package glsa + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models" + "glsamaker/pkg/models/cve" + "net/http" + "strconv" +) + +// Show renders a template to show the landing page of the application +func Delete(w http.ResponseWriter, r *http.Request) { + + // TODO delete confidential bugs? + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.Delete { + authentication.AccessDenied(w, r) + return + } + + glsaID := r.URL.Path[len("/glsa/delete/"):] + + if _, err := strconv.Atoi(glsaID); err != nil { + http.Redirect(w, r, "/", 301) + w.Write([]byte("err")) + } + + var glsa *models.Glsa + var glsaToBug *models.GlsaToBug + var comment *cve.Comment + connection.DB.Model(glsa).Where("id = ?", glsaID).Delete() + connection.DB.Model(glsaToBug).Where("glsa_id = ?", glsaID).Delete() + connection.DB.Model(comment).Where("glsa_id = ?", glsaID).Delete() + + w.Write([]byte("ok")) +} diff --git a/pkg/app/handler/glsa/edit.go b/pkg/app/handler/glsa/edit.go new file mode 100644 index 0000000..04e67b3 --- /dev/null +++ b/pkg/app/handler/glsa/edit.go @@ -0,0 +1,185 @@ +package glsa + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/bugzilla" + "glsamaker/pkg/models/gpackage" + "net/http" + "strconv" + "time" +) + +func getStringParam(key string, r *http.Request) string { + if len(r.Form[key]) > 0 { + return r.Form[key][0] + } + + return "" +} + +func getArrayParam(key string, r *http.Request) []string { + return r.Form[key] +} + +// Show renders a template to show the landing page of the application +func Edit(w http.ResponseWriter, r *http.Request) { + + // TODO edit confidential bugs? + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.Edit { + authentication.AccessDenied(w, r) + return + } + + glsaID := r.URL.Path[len("/glsa/edit/"):] + + parsedGlsaId, _ := strconv.ParseInt(glsaID, 10, 64) + currentGlsa := &models.Glsa{Id: parsedGlsaId} + err := user.CanAccess(connection.DB.Model(currentGlsa). + Relation("Bugs"). + Relation("Creator"). + Relation("Comments"). + WherePK()). + Select() + + if r.Method == "POST" { + + r.ParseForm() + + id, err := strconv.ParseInt(glsaID, 10, 64) + + if err != nil { + http.NotFound(w, r) + return + } + + // if + var packages []gpackage.Package + for k, _ := range getArrayParam("package_atom", r) { + newPackage := gpackage.Package{ + Affected: r.Form["package_vulnerable"][k] == "true", + Atom: r.Form["package_atom"][k], + Identifier: r.Form["package_identifier"][k], + Version: r.Form["package_version"][k], + Slot: r.Form["package_slot"][k], + Arch: r.Form["package_arch"][k], + Auto: r.Form["package_auto"][k] == "true", + } + packages = append(packages, newPackage) + } + + var references []models.Reference + for k, _ := range getArrayParam("reference_title", r) { + newReference := models.Reference{ + Title: r.Form["reference_title"][k], + URL: r.Form["reference_url"][k], + } + references = append(references, newReference) + } + + // Update Bugs: delete old mapping first + _, err = connection.DB.Model(&[]models.GlsaToBug{}).Where("glsa_id = ?", glsaID).Delete() + if err != nil { + logger.Error.Println("ERR during delete") + logger.Error.Println(err) + } + + newBugs := bugzilla.GetBugsByIds(getArrayParam("bugs", r)) + + for _, newBug := range newBugs { + _, err = connection.DB.Model(&newBug).OnConflict("(id) DO UPDATE").Insert() + + if err != nil { + logger.Error.Println("Error creating bug") + logger.Error.Println(err) + } + + parsedGlsaID, _ := strconv.ParseInt(glsaID, 10, 64) + + glsaToBug := &models.GlsaToBug{ + GlsaId: parsedGlsaID, + BugId: newBug.Id, + } + + connection.DB.Model(glsaToBug).Insert() + + } + + glsa := &models.Glsa{ + Id: id, + // Alias: getStringParam("alias", r), + // Type: getStringParam("status", r), + Title: getStringParam("title", r), + Synopsis: getStringParam("synopsis", r), + Packages: packages, + Description: getStringParam("description", r), + Impact: getStringParam("impact", r), + Workaround: getStringParam("workaround", r), + Resolution: getStringParam("resolution", r), + References: references, + Permission: getStringParam("permission", r), + Access: getStringParam("access", r), + Severity: getStringParam("severity", r), + Keyword: getStringParam("keyword", r), + Background: getStringParam("background", r), + //TODO + //Bugs: , + //Comments: nil, + Revision: "r9999", + // Created: time.Time{}, + Updated: time.Time{}, + } + + if currentGlsa.Type == "request" && glsa.Description != "" { + glsa.Type = "draft" + } else { + glsa.Type = currentGlsa.Type + } + + _, err = connection.DB.Model(glsa).Column( + "type", + "title", + "synopsis", + "packages", + "description", + "impact", + "workaround", + "resolution", + "references", + "permission", + "access", + "severity", + "keyword", + "background", + "updated", + "revision").WherePK().Update() + + if err != nil { + http.NotFound(w, r) + logger.Error.Println("ERR NOT FOUND") + logger.Error.Println(err) + return + } + + http.Redirect(w, r, "/glsa/"+glsaID, 301) + return + } + + if err != nil { + http.NotFound(w, r) + return + } + + currentGlsa.ComputeStatus(user) + currentGlsa.ComputeCommentBadges() + + glsaCount, err := connection.DB.Model((*models.Glsa)(nil)).Count() + + renderEditTemplate(w, user, currentGlsa, int64(glsaCount)) +} diff --git a/pkg/app/handler/glsa/release.go b/pkg/app/handler/glsa/release.go new file mode 100644 index 0000000..c24e5b0 --- /dev/null +++ b/pkg/app/handler/glsa/release.go @@ -0,0 +1,70 @@ +package glsa + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "net/http" + "strconv" + "strings" + "time" +) + +// Show renders a template to show the landing page of the application +func Release(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.Release { + authentication.AccessDenied(w, r) + return + } + + glsaID := r.URL.Path[len("/glsa/release/"):] + + currentGlsa := new(models.Glsa) + err := user.CanAccess(connection.DB.Model(currentGlsa). + Where("id = ?", glsaID)). + Select() + + if err != nil { + http.NotFound(w, r) + return + } + + currentGlsa.Type = "glsa" + currentGlsa.Alias = computeNextGLSAId() + + _, err = connection.DB.Model(currentGlsa).Column("type").WherePK().Update() + _, err = connection.DB.Model(currentGlsa).Column("alias").WherePK().Update() + + http.Redirect(w, r, "/archive", 301) +} + +func computeNextGLSAId() string { + + logger.Info.Println("compute Next GLSA") + + newGLSAID := "" + var glsas []*models.Glsa + err := connection.DB.Model(&glsas).Where("type = ?", "glsa").Order("alias DESC").Limit(1).Select() + + if err != nil || len(glsas) == 0 { + newGLSAID = time.Now().Format("200601") + "-" + "01" + } else if !strings.HasPrefix(glsas[0].Alias, time.Now().Format("200601")+"-") { + newGLSAID = time.Now().Format("200601") + "-" + "01" + } else { + oldId := strings.Replace(glsas[0].Alias, time.Now().Format("200601")+"-", "", 1) + parsedOldId, _ := strconv.Atoi(oldId) + parsedOldId = parsedOldId + 1 + newID := strconv.Itoa(parsedOldId) + if len(newID) < 2 { + newID = "0" + newID + } + newGLSAID = time.Now().Format("200601") + "-" + newID + } + + return newGLSAID +} diff --git a/pkg/app/handler/glsa/utils.go b/pkg/app/handler/glsa/utils.go new file mode 100644 index 0000000..b417a80 --- /dev/null +++ b/pkg/app/handler/glsa/utils.go @@ -0,0 +1,79 @@ +package glsa + +import ( + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/bugzilla" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderViewTemplate(w http.ResponseWriter, user *users.User, glsa *models.Glsa, glsaCount int64) { + + templates := template.Must( + template.Must( + template.New("Show"). + Funcs(getFuncMap()). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/glsa/show.tmpl")) + + templates.ExecuteTemplate(w, "show.tmpl", createPageData("show", user, glsa, glsaCount)) +} + +// renderIndexTemplate renders all templates used for the landing page +func renderEditTemplate(w http.ResponseWriter, user *users.User, glsa *models.Glsa, glsaCount int64) { + templates := template.Must( + template.Must( + template.New("Show"). + Funcs(getFuncMap()). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/glsa/edit.tmpl")) + + templates.ExecuteTemplate(w, "edit.tmpl", createPageData("edit", user, glsa, glsaCount)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, glsa *models.Glsa, glsaCount int64) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + Glsa *models.Glsa + GlsaCount int64 + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + Glsa: glsa, + GlsaCount: glsaCount, + } +} + +func getFuncMap() template.FuncMap { + return template.FuncMap{ + "bugIsReady": BugIsReady, + "prevGLSA": PrevGLSA, + "nextGLSA": NextGLSA, + } +} + +func BugIsReady(bug bugzilla.Bug) bool { + return bug.IsReady() +} + +func PrevGLSA(id int64, min int64) int64 { + logger.Info.Println("prev glsa") + if id == min { + return id + } + return id - 1 +} + +func NextGLSA(id int64, max int64) int64 { + if id == max { + return id + } + return id + 1 +} diff --git a/pkg/app/handler/glsa/view.go b/pkg/app/handler/glsa/view.go new file mode 100644 index 0000000..d84273c --- /dev/null +++ b/pkg/app/handler/glsa/view.go @@ -0,0 +1,48 @@ +package glsa + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models" + "net/http" + "strconv" +) + +// Show renders a template to show the landing page of the application +func View(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.View { + authentication.AccessDenied(w, r) + return + } + + glsaID := r.URL.Path[len("/glsa/"):] + + parsedGlsaId, _ := strconv.ParseInt(glsaID, 10, 64) + glsa := &models.Glsa{Id: parsedGlsaId} + err := user.CanAccess(connection.DB.Model(glsa). + Relation("Bugs"). + Relation("Creator"). + Relation("Comments").WherePK()). + Select() + + if err != nil { + http.NotFound(w, r) + return + } + + if glsa.Permission == "confidential" && user.Confidential() != "confidential" { + authentication.AccessDenied(w, r) + return + } + + glsa.ComputeStatus(user) + glsa.ComputeCommentBadges() + + glsaCount, err := connection.DB.Model((*models.Glsa)(nil)).Count() + + renderViewTemplate(w, user, glsa, int64(glsaCount)) +} diff --git a/pkg/app/handler/home/index.go b/pkg/app/handler/home/index.go new file mode 100644 index 0000000..8f514f7 --- /dev/null +++ b/pkg/app/handler/home/index.go @@ -0,0 +1,16 @@ +// Used to show the landing page of the application + +package home + +import ( + "glsamaker/pkg/app/handler/authentication/utils" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + renderHomeTemplate(w, user) +} diff --git a/pkg/app/handler/home/utils.go b/pkg/app/handler/home/utils.go new file mode 100644 index 0000000..5f6e0b5 --- /dev/null +++ b/pkg/app/handler/home/utils.go @@ -0,0 +1,34 @@ +// miscellaneous utility functions used for the landing page of the application + +package home + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderHomeTemplate(w http.ResponseWriter, user *users.User) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/home/*.tmpl")) + + templates.ExecuteTemplate(w, "home.tmpl", createPageData("home", user)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + } +} diff --git a/pkg/app/handler/newRequest/index.go b/pkg/app/handler/newRequest/index.go new file mode 100644 index 0000000..929ac5b --- /dev/null +++ b/pkg/app/handler/newRequest/index.go @@ -0,0 +1,186 @@ +// Used to show the landing page of the application + +package newRequest + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/app/handler/glsa" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/bugzilla" + "glsamaker/pkg/models/cve" + "crypto/sha256" + "fmt" + "github.com/go-pg/pg/v9" + "net/http" + "strconv" + "strings" + "time" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.Create { + authentication.AccessDenied(w, r) + return + } + + bugs, title, synopsis, description, workaround, impact, background, resolution, importReferences, permissions, access, severity, keyword, comment, err := getParams(r) + newID := getNextGLSAId() + + if err != nil || bugs == "" { + // render without message + renderNewTemplate(w, user, strconv.FormatInt(newID, 10)) + return + } + + // create bugs + newBugs := bugzilla.GetBugsByIds(strings.Split(bugs, ",")) + + for _, newBug := range newBugs { + _, err = connection.DB.Model(&newBug).OnConflict("(id) DO UPDATE").Insert() + + if err != nil { + logger.Error.Println("Error creating bug") + logger.Error.Println(err) + } + + glsaToBug := &models.GlsaToBug{ + GlsaId: newID, + BugId: newBug.Id, + } + + connection.DB.Model(glsaToBug).Insert() + + } + + var references []models.Reference + + // TODO if title is empty try to import from bug + // TODO validate permissions + if importReferences { + // TODO import references + + // import from CVE + for _, bug := range strings.Split(bugs, ",") { + var cves []cve.DefCveItem + connection.DB.Model(&cves).Where("bugs::jsonb @> ?", "\""+bug+"\"").Select() + + for _, cve := range cves { + references = append(references, models.Reference{ + Title: cve.Id, + URL: "https://nvd.nist.gov/vuln/detail/" + cve.Id, + }) + } + + } + + // import from BUG + for _, bug := range newBugs { + for _, alias := range bug.Alias { + if strings.HasPrefix(alias, "CVE-") { + alreadyPresent := false + for _, reference := range references { + if reference.Title == alias { + alreadyPresent = true + } + } + if !alreadyPresent { + references = append(references, models.Reference{ + Title: alias, + URL: "https://nvd.nist.gov/vuln/detail/" + alias, + }) + } + } + } + } + + } + + id := title + bugs + time.Now().String() + id = fmt.Sprintf("%x", sha256.Sum256([]byte(id))) + + glsaType := "request" + if description != "" { + glsaType = "draft" + } + + newGlsa := &models.Glsa{ + //Id: id, + Type: glsaType, + Title: title, + Synopsis: synopsis, + Description: description, + Workaround: workaround, + Impact: impact, + Background: background, + Resolution: resolution, + References: references, + Permission: permissions, + Access: access, + Severity: severity, + Keyword: keyword, + Revision: "r0", + CreatorId: user.Id, + Created: time.Now(), + Updated: time.Now(), + } + + _, err = connection.DB.Model(newGlsa).OnConflict("(id) DO Nothing").Insert() + if err != nil { + logger.Error.Println("Err during creating new GLSA") + logger.Error.Println(err) + } + + if comment != "" { + glsa.AddNewCommment(strconv.FormatInt(newID, 10), user, comment, "comment") + } + + if glsaType == "draft" { + http.Redirect(w, r, "/drafts", 301) + } else { + http.Redirect(w, r, "/requests", 301) + } +} + +func getParams(r *http.Request) (string, string, string, string, string, string, string, string, bool, string, string, string, string, string, error) { + err := r.ParseForm() + if err != nil { + return "", "", "", "", "", "", "", "", false, "", "", "", "", "", err + } + bugs := r.Form.Get("bugs") + title := r.Form.Get("title") + synopsis := r.Form.Get("synopsis") + description := r.Form.Get("description") + workaround := r.Form.Get("workaround") + impact := r.Form.Get("impact") + background := r.Form.Get("background") + resolution := r.Form.Get("resolution") + importReferences := r.Form.Get("importReferences") + permissions := r.Form.Get("permissions") + access := r.Form.Get("access") + severity := r.Form.Get("severity") + keyword := r.Form.Get("keyword") + comment := r.Form.Get("comment") + return bugs, title, synopsis, description, workaround, impact, background, resolution, importReferences == "on", permissions, access, severity, keyword, comment, err +} + +func getNextGLSAId() int64 { + var newID int64 + newID = 1 + var glsas []*models.Glsa + err := connection.DB.Model(&glsas).Order("id DESC").Limit(1).Select() + + if err != nil && err != pg.ErrNoRows { + newID = -1 + } else if glsas != nil && len(glsas) == 1 { + newID = glsas[0].Id + 1 + } + + return newID +} diff --git a/pkg/app/handler/newRequest/utils.go b/pkg/app/handler/newRequest/utils.go new file mode 100644 index 0000000..7192939 --- /dev/null +++ b/pkg/app/handler/newRequest/utils.go @@ -0,0 +1,36 @@ +// miscellaneous utility functions used for the landing page of the application + +package newRequest + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderNewTemplate(w http.ResponseWriter, user *users.User, newID string) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/new/*.tmpl")) + + templates.ExecuteTemplate(w, "new.tmpl", createPageData("new", user, newID)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, newID string) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + NewID string + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + NewID: newID, + } +} diff --git a/pkg/app/handler/requests/index.go b/pkg/app/handler/requests/index.go new file mode 100644 index 0000000..fddbd86 --- /dev/null +++ b/pkg/app/handler/requests/index.go @@ -0,0 +1,44 @@ +// Used to show the landing page of the application + +package requests + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + if !user.Permissions.Glsa.View { + authentication.AccessDenied(w, r) + return + } + + var requests []*models.Glsa + err := user.CanAccess(connection.DB.Model(&requests). + Where("type = ?", "request"). + Relation("Bugs"). + Relation("Creator"). + Relation("Comments")). + Select() + + if err != nil { + logger.Info.Println("Error during request selection") + logger.Info.Println(err) + http.NotFound(w, r) + return + } + + for _, request := range requests { + request.ComputeStatus(user) + } + + renderRequestsTemplate(w, user, requests) +} diff --git a/pkg/app/handler/requests/utils.go b/pkg/app/handler/requests/utils.go new file mode 100644 index 0000000..85bc472 --- /dev/null +++ b/pkg/app/handler/requests/utils.go @@ -0,0 +1,36 @@ +// miscellaneous utility functions used for the landing page of the application + +package requests + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderRequestsTemplate(w http.ResponseWriter, user *users.User, requests []*models.Glsa) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/requests/*.tmpl")) + + templates.ExecuteTemplate(w, "requests.tmpl", createPageData("requests", user, requests)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, requests []*models.Glsa) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + Requests []*models.Glsa + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + Requests: requests, + } +} diff --git a/pkg/app/handler/search/index.go b/pkg/app/handler/search/index.go new file mode 100644 index 0000000..1503d9c --- /dev/null +++ b/pkg/app/handler/search/index.go @@ -0,0 +1,126 @@ +// Used to show the landing page of the application + +package search + +import ( + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "github.com/go-pg/pg/v9/orm" + "net/http" + "strconv" +) + +// Show renders a template to show the landing page of the application +func Search(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + keys, ok := r.URL.Query()["q"] + + if !ok || len(keys[0]) < 1 { + http.NotFound(w, r) + return + } + + // Query()["key"] will return an array of items, + // we only want the single item. + key := keys[0] + + // redirect to glsa if isNumeric + if _, err := strconv.Atoi(key); err == nil { + http.Redirect(w, r, "/glsa/"+key, 301) + } + + if key == "#home" { + http.Redirect(w, r, "/", 301) + return + } else if key == "#dashboard" { + http.Redirect(w, r, "/dashboard", 301) + return + } else if key == "#new" { + http.Redirect(w, r, "/new", 301) + return + } else if key == "#cvetool" { + http.Redirect(w, r, "/cve/tool", 301) + return + } else if key == "#requests" { + http.Redirect(w, r, "/requests", 301) + return + } else if key == "#drafts" { + http.Redirect(w, r, "/drafts", 301) + return + } else if key == "#all" { + http.Redirect(w, r, "/all", 301) + return + } else if key == "#archive" { + http.Redirect(w, r, "/archive", 301) + return + } else if key == "#about" { + http.Redirect(w, r, "/about", 301) + return + } else if key == "#bugzilla" { + http.Redirect(w, r, "https://bugs.gentoo.org/", 301) + return + } else if key == "#admin" { + http.Redirect(w, r, "/admin", 301) + return + } else if key == "#password" { + http.Redirect(w, r, "/account/password", 301) + return + } else if key == "#2fa" { + http.Redirect(w, r, "/account/2fa", 301) + return + } else if key == "#statistics" { + http.Redirect(w, r, "/statistics", 301) + return + } + + if key == "#logout" { + http.Redirect(w, r, "/logout", 301) + return + } + + if !user.Permissions.Glsa.View { + authentication.AccessDenied(w, r) + return + } + + var glsas []*models.Glsa + err := user.CanAccess(connection.DB.Model(&glsas). + Relation("Bugs"). + Relation("Comments"). + Relation("Creator"). + WhereGroup(func(q *orm.Query) (*orm.Query, error) { + q = q.WhereOr("title LIKE " + "'%" + key + "%'"). + WhereOr("type LIKE " + "'%" + key + "%'"). + WhereOr("synopsis LIKE " + "'%" + key + "%'"). + WhereOr("description LIKE " + "'%" + key + "%'"). + WhereOr("workaround LIKE " + "'%" + key + "%'"). + WhereOr("resolution LIKE " + "'%" + key + "%'"). + WhereOr("keyword LIKE " + "'%" + key + "%'"). + WhereOr("background LIKE " + "'%" + key + "%'") + //WhereOr("creator LIKE " + "'%" + key + "%'") + return q, nil + })). + Select() + + // TODO search in comments + // TODO search in bugs + + if err != nil { + logger.Info.Println("Error during searching") + logger.Info.Println(err) + http.NotFound(w, r) + return + } + + for _, glsa := range glsas { + glsa.ComputeStatus(user) + } + + renderSearchTemplate(w, user, key, glsas) + +} diff --git a/pkg/app/handler/search/utils.go b/pkg/app/handler/search/utils.go new file mode 100644 index 0000000..81e4e88 --- /dev/null +++ b/pkg/app/handler/search/utils.go @@ -0,0 +1,38 @@ +// miscellaneous utility functions used for the landing page of the application + +package search + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderSearchTemplate(w http.ResponseWriter, user *users.User, searchQuery string, searchResults []*models.Glsa) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/search/*.tmpl")) + + templates.ExecuteTemplate(w, "search.tmpl", createPageData("search", user, searchQuery, searchResults)) +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, searchQuery string, searchResults []*models.Glsa) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + GLSAs []*models.Glsa + SearchQuery string + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + GLSAs: searchResults, + SearchQuery: searchQuery, + } +} diff --git a/pkg/app/handler/statistics/index.go b/pkg/app/handler/statistics/index.go new file mode 100644 index 0000000..d4b3a4d --- /dev/null +++ b/pkg/app/handler/statistics/index.go @@ -0,0 +1,42 @@ +// Used to show the landing page of the application + +package statistics + +import ( + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models" + "glsamaker/pkg/models/cve" + "net/http" +) + +// Show renders a template to show the landing page of the application +func Show(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + requests, _ := connection.DB.Model((*models.Glsa)(nil)).Where("type = ?", "request").Count() + drafts, _ := connection.DB.Model((*models.Glsa)(nil)).Where("type = ?", "draft").Count() + glsas, _ := connection.DB.Model((*models.Glsa)(nil)).Where("type = ?", "glsa").Count() + allGlsas, _ := connection.DB.Model((*models.Glsa)(nil)).Count() + + new, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "New").Count() + assigned, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "Assigned").Count() + nfu, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "NFU").Count() + later, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "Later").Count() + invalid, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Where("state = ?", "Invalid").Count() + allCVEs, _ := connection.DB.Model((*cve.DefCveItem)(nil)).Count() + + statisticsData := StatisticsData{ + Requests: float64(requests) / float64(allGlsas), + Drafts: float64(drafts) / float64(allGlsas), + Glsas: float64(glsas) / float64(allGlsas), + New: float64(new) / float64(allCVEs), + Assigned: float64(assigned) / float64(allCVEs), + NFU: float64(nfu) / float64(allCVEs), + Later: float64(later) / float64(allCVEs), + Invalid: float64(invalid) / float64(allCVEs), + } + + renderStatisticsTemplate(w, user, &statisticsData) +} diff --git a/pkg/app/handler/statistics/utils.go b/pkg/app/handler/statistics/utils.go new file mode 100644 index 0000000..3e64ddf --- /dev/null +++ b/pkg/app/handler/statistics/utils.go @@ -0,0 +1,48 @@ +// miscellaneous utility functions used for the landing page of the application + +package statistics + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the landing page +func renderStatisticsTemplate(w http.ResponseWriter, user *users.User, statisticsData *StatisticsData) { + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/statistics/*.tmpl")) + + templates.ExecuteTemplate(w, "statistics.tmpl", createPageData("statistics", user, statisticsData)) +} + +type StatisticsData struct { + Requests float64 + Drafts float64 + Glsas float64 + // CVEs + New float64 + Assigned float64 + NFU float64 + Later float64 + Invalid float64 +} + +// createPageData creates the data used in the template of the landing page +func createPageData(page string, user *users.User, statisticsData *StatisticsData) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + Data *StatisticsData + }{ + Page: page, + Application: models.GetDefaultGlobalSettings(), + User: user, + Data: statisticsData, + } +} diff --git a/pkg/app/serve.go b/pkg/app/serve.go new file mode 100644 index 0000000..1f16d9a --- /dev/null +++ b/pkg/app/serve.go @@ -0,0 +1,214 @@ +// Entrypoint for the web application + +package app + +import ( + "glsamaker/pkg/app/handler/about" + "glsamaker/pkg/app/handler/account" + "glsamaker/pkg/app/handler/admin" + "glsamaker/pkg/app/handler/all" + "glsamaker/pkg/app/handler/archive" + "glsamaker/pkg/app/handler/authentication" + "glsamaker/pkg/app/handler/authentication/totp" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/app/handler/authentication/webauthn" + "glsamaker/pkg/app/handler/cvetool" + "glsamaker/pkg/app/handler/dashboard" + "glsamaker/pkg/app/handler/drafts" + "glsamaker/pkg/app/handler/glsa" + "glsamaker/pkg/app/handler/home" + "glsamaker/pkg/app/handler/newRequest" + "glsamaker/pkg/app/handler/requests" + "glsamaker/pkg/app/handler/search" + "glsamaker/pkg/app/handler/statistics" + "glsamaker/pkg/config" + "glsamaker/pkg/database" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "log" + "net/http" + "strings" +) + +// Serve is used to serve the web application +func Serve() { + + database.Connect() + defer connection.DB.Close() + + CreateDefaultAdmin() + models.SeedInitialApplicationData() + + // public login page + loginPage("/login", authentication.Login) + + // second factor login page + // (either totp or webauthn, depending on the user settings) + twoFactorLogin("/login/2fa", authentication.SecondFactorLogin) + + // webauthn login endpoints + twoFactorLogin("/login/2fa/totp", totp.Login) + + // webauthn login endpoints + twoFactorLogin("/login/2fa/webauthn/begin", webauthn.BeginLogin) + twoFactorLogin("/login/2fa/webauthn/finish", webauthn.FinishLogin) + + requireLogin("/", home.Show) + + requireLogin("/dashboard", dashboard.Show) + + requireLogin("/statistics", statistics.Show) + + requireLogin("/search", search.Search) + + requireLogin("/about", about.Show) + requireLogin("/about/search", about.ShowSearch) + requireLogin("/about/cli", about.ShowCLI) + + requireLogin("/archive", archive.Show) + + requireLogin("/drafts", drafts.Show) + + requireLogin("/requests", requests.Show) + + requireLogin("/all", all.Show) + + requireLogin("/new", newRequest.Show) + + requireLogin("/cve/update", cvetool.Update) + requireLogin("/cve/tool", cvetool.Show) + requireLogin("/cve/tool/fullscreen", cvetool.ShowFullscreen) + requireLogin("/cve/data", cvetool.CveData) + requireLogin("/cve/add", cvetool.Add) + requireLogin("/cve/comment/add", cvetool.AddComment) + requireLogin("/cve/bug/assign", cvetool.AssignBug) + requireLogin("/cve/state/change", cvetool.ChangeState) + + requireLogin("/logout", authentication.Logout) + + requireLogin("/account/password", account.ChangePassword) + requireLogin("/account/2fa", account.TwoFactorAuth) + requireLogin("/account/2fa/notice/disable", account.Disable2FANotice) + requireLogin("/account/2fa/totp/activate", account.ActivateTOTP) + requireLogin("/account/2fa/totp/disable", account.DisableTOTP) + requireLogin("/account/2fa/totp/verify", account.VerifyTOTP) + requireLogin("/account/2fa/webauthn/activate", account.ActivateWebAuthn) + requireLogin("/account/2fa/webauthn/disable", account.DisableWebAuthn) + requireLogin("/account/2fa/webauthn/register/begin", webauthn.BeginRegistration) + requireLogin("/account/2fa/webauthn/register/finish", webauthn.FinishRegistration) + + requireLogin("/glsa/", glsa.View) + requireLogin("/glsa/edit/", glsa.Edit) + requireLogin("/glsa/comment/add", glsa.AddComment) + requireLogin("/glsa/delete/", glsa.Delete) + requireLogin("/glsa/release/", glsa.Release) + requireLogin("/glsa/bugs/update", glsa.UpdateBugs) + + requireAdmin("/admin", admin.Show) + requireAdmin("/admin/", admin.Show) + requireAdmin("/admin/edit/users", admin.EditUsers) + requireAdmin("/admin/edit/permissions", admin.EditPermissions) + requireAdmin("/admin/edit/password/reset/", admin.ResetPassword) + + fs := http.StripPrefix("/assets/", http.FileServer(http.Dir("/go/src/glsamaker/assets"))) + requireLogin("/assets/", fs.ServeHTTP) + + logger.Info.Println("Serving on port " + config.Port()) + log.Fatal(http.ListenAndServe(":"+config.Port(), nil)) +} + +func loginPage(path string, handler http.HandlerFunc) { + http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + setDefaultHeaders(w) + + if utils.IsAuthenticated(w, r) { + http.Redirect(w, r, "/", 301) + } else if utils.Only2FAMissing(w, r) { + http.Redirect(w, r, "/login/2fa", 301) + } else { + handler(w, r) + } + }) +} + +func twoFactorLogin(path string, handler http.HandlerFunc) { + http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + setDefaultHeaders(w) + + if utils.IsAuthenticated(w, r) { + http.Redirect(w, r, "/", 301) + } else if utils.Only2FAMissing(w, r) { + handler(w, r) + } else { + http.Redirect(w, r, "/login", 301) + } + }) +} + +// define a route using the default middleware and the given handler +func requireLogin(path string, handler http.HandlerFunc) { + http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + setDefaultHeaders(w) + + if utils.IsAuthenticatedAndNeedsNewPassword(w, r) { + if strings.HasPrefix(path, "/logout") || + strings.HasPrefix(path, "/assets/") || + strings.HasPrefix(path, "/account/password") { + handler(w, r) + } else { + http.Redirect(w, r, "/account/password", 301) + } + } else if utils.IsAuthenticatedAndNeeds2FA(w, r) { + if strings.HasPrefix(path, "/logout") || + strings.HasPrefix(path, "/assets/") || + strings.HasPrefix(path, "/account/2fa") { + handler(w, r) + } else { + http.Redirect(w, r, "/account/2fa", 301) + } + } else if utils.IsAuthenticated(w, r) { + handler(w, r) + } else if utils.Only2FAMissing(w, r) { + http.Redirect(w, r, "/login/2fa", 301) + } else { + http.Redirect(w, r, "/login", 301) + } + }) +} + +// define a route using the default middleware and the given handler +func requireAdmin(path string, handler http.HandlerFunc) { + http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + setDefaultHeaders(w) + + if utils.IsAuthenticatedAndNeedsNewPassword(w, r) { + if strings.HasPrefix(path, "/logout") || + strings.HasPrefix(path, "/assets/") || + strings.HasPrefix(path, "/account/password") { + handler(w, r) + } else { + http.Redirect(w, r, "/account/password", 301) + } + } else if utils.IsAuthenticatedAndNeeds2FA(w, r) { + if strings.HasPrefix(path, "/logout") || + strings.HasPrefix(path, "/assets/") || + strings.HasPrefix(path, "/account/2fa") { + handler(w, r) + } else { + http.Redirect(w, r, "/account/2fa", 301) + } + } else if utils.IsAuthenticatedAsAdmin(w, r) { + handler(w, r) + } else if utils.IsAuthenticated(w, r) { + authentication.AccessDenied(w, r) + } else { + http.Redirect(w, r, "/login", 301) + } + }) +} + +// setDefaultHeaders sets the default headers that apply for all pages +func setDefaultHeaders(w http.ResponseWriter) { + w.Header().Set("Cache-Control", "no-store") +} diff --git a/pkg/app/utils.go b/pkg/app/utils.go new file mode 100644 index 0000000..9d66c13 --- /dev/null +++ b/pkg/app/utils.go @@ -0,0 +1,89 @@ +package app + +import ( + "glsamaker/pkg/app/handler/authentication/totp" + "glsamaker/pkg/config" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models/users" +) + +func defaultAdminPermissions() users.Permissions { + return users.Permissions{ + Glsa: users.GlsaPermissions{ + View: true, + UpdateBugs: true, + Comment: true, + Create: true, + Edit: true, + Approve: true, + ApproveOwnGlsa: true, + Decline: true, + Delete: true, + Release: true, + Confidential: true, + }, + CVETool: users.CVEToolPermissions{ + View: true, + UpdateCVEs: true, + Comment: true, + AddPackage: true, + ChangeState: true, + AssignBug: true, + CreateBug: true, + }, + Admin: users.AdminPermissions{ + View: true, + CreateTemplates: true, + ManageUsers: true, + GlobalSettings: true, + }, + } +} + +func CreateDefaultAdmin() { + + token, qrcode := totp.Generate(config.AdminEmail()) + + badge := users.Badge{ + Name: "admin", + Description: "Admin Account", + Color: "orange", + } + + passwordParameters := users.Argon2Parameters{ + Type: "argon2id", + Time: 1, + Memory: 64 * 1024, + Threads: 4, + KeyLen: 32, + } + passwordParameters.GenerateSalt(32) + passwordParameters.GeneratePassword(config.AdminInitialPassword()) + + defaultUser := &users.User{ + Email: config.AdminEmail(), + Password: passwordParameters, + Nick: "admin", + Name: "Admin Account", + Role: "admin", + ForcePasswordChange: false, + TOTPSecret: token, + TOTPQRCode: qrcode, + IsUsingTOTP: false, + WebauthnCredentials: nil, + IsUsingWebAuthn: false, + Show2FANotice: true, + Badge: badge, + Disabled: false, + ForcePasswordRotation: false, + Force2FA: false, + Permissions: defaultAdminPermissions(), + } + + _, err := connection.DB.Model(defaultUser).OnConflict("(email) DO Nothing").Insert() + if err != nil { + logger.Error.Println("Err during creating default admin user") + logger.Error.Println(err) + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..bab084d --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,63 @@ +package config + +import "os" + +func PostgresUser() string { + return getEnv("GLSAMAKER_POSTGRES_USER", "root") +} + +func PostgresPass() string { + return getEnv("GLSAMAKER_POSTGRES_PASS", "root") +} + +func PostgresDb() string { + return getEnv("GLSAMAKER_POSTGRES_DB", "glsamaker") +} + +func PostgresHost() string { + return getEnv("GLSAMAKER_POSTGRES_HOST", "db") +} + +func PostgresPort() string { + return getEnv("GLSAMAKER_POSTGRES_PORT", "5432") +} + +func Debug() string { + return getEnv("GLSAMAKER_DEBUG", "false") +} + +func Quiet() string { + return getEnv("GLSAMAKER_QUIET", "false") +} + +func LogFile() string { + return getEnv("GLSAMAKER_LOG_FILE", "/var/log/glsamaker/errors.log") +} + +func Version() string { + return getEnv("GLSAMAKER_VERSION", "v0.1.0") +} + +func Port() string { + return getEnv("GLSAMAKER_PORT", "5000") +} + +func AdminEmail() string { + return getEnv("GLSAMAKER_EMAIL", "admin@gentoo.org") +} + +func AdminInitialPassword() string { + return getEnv("GLSAMAKER_INITIAL_ADMIN_PASSWORD", "admin") +} + +func CacheControl() string { + return getEnv("GLSAMAKER_CACHE_CONTROL", "max-age=300") +} + +func getEnv(key string, fallback string) string { + if os.Getenv(key) != "" { + return os.Getenv(key) + } else { + return fallback + } +} diff --git a/pkg/cveimport/update.go b/pkg/cveimport/update.go new file mode 100644 index 0000000..a15e447 --- /dev/null +++ b/pkg/cveimport/update.go @@ -0,0 +1,96 @@ +package cveimport + +import ( + "glsamaker/pkg/database" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/cve" + "compress/gzip" + "encoding/json" + "io" + "io/ioutil" + "net/http" + "strconv" +) + +func Update() { + database.Connect() + defer connection.DB.Close() + + logger.Info.Println("Start update...") + IncrementalCVEImport() + logger.Info.Println("Finished update...") +} + +func FullUpdate() { + database.Connect() + defer connection.DB.Close() + + logger.Info.Println("Start full update...") + FullCVEImport() + logger.Info.Println("Finished full update...") +} + +func IncrementalCVEImport() { + logger.Info.Println("Start importing recent CVEs") + importCVEs("recent") + logger.Info.Println("Finished importing recent CVEs") +} + +func FullCVEImport() { + for i := 2002; i <= 2020; i++ { + year := strconv.Itoa(i) + logger.Info.Println("Import CVEs from " + year) + importCVEs(year) + logger.Info.Println("Finished importing recent CVEs") + } +} + +func importCVEs(year string) { + resp, err := http.Get("https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-" + year + ".json.gz") + if err != nil { + logger.Error.Println("err") + logger.Error.Println(err) + return + } + defer resp.Body.Close() + + var reader io.ReadCloser + reader, err = gzip.NewReader(resp.Body) + defer reader.Close() + + s, _ := ioutil.ReadAll(reader) + + var data cve.NVDFeed + + err = json.Unmarshal([]byte(s), &data) + + if err != nil { + logger.Info.Println("ERROR during unmarshal:") + logger.Info.Println(err) + } + + for _, cveitem := range data.CVEItems { + cveitem.Id = cveitem.Cve.CVEDataMeta.ID + cveitem.State = "New" + + description := "" + for _, langstring := range cveitem.Cve.Description.DescriptionData { + if langstring.Lang == "en" { + description = langstring.Value + } + } + cveitem.Description = description + + _, err := connection.DB.Model(cveitem).OnConflict("(id) DO UPDATE").Insert() + if err != nil { + logger.Error.Println("Err during CVE insert") + logger.Error.Println(err) + } + } + + // update the time of the last bug update + models.SetApplicationValue("LastCVEUpdate", "") + +} diff --git a/pkg/database/connection/connection.go b/pkg/database/connection/connection.go new file mode 100644 index 0000000..733aee2 --- /dev/null +++ b/pkg/database/connection/connection.go @@ -0,0 +1,42 @@ +// Contains utility functions around the database + +package connection + +import ( + "glsamaker/pkg/config" + "glsamaker/pkg/logger" + "context" + "github.com/go-pg/pg/v9" +) + +// DBCon is the connection handle +// for the database +var ( + DB *pg.DB +) + +type dbLogger struct{} + +func (d dbLogger) BeforeQuery(c context.Context, q *pg.QueryEvent) (context.Context, error) { + return c, nil +} + +// AfterQuery is used to log SQL queries +func (d dbLogger) AfterQuery(c context.Context, q *pg.QueryEvent) error { + logger.Debug.Println(q.FormattedQuery()) + return nil +} + +// Connect is used to connect to the database +// and turn on logging if desired +func Connect() { + DB = pg.Connect(&pg.Options{ + User: config.PostgresUser(), + Password: config.PostgresPass(), + Database: config.PostgresDb(), + Addr: config.PostgresHost() + ":" + config.PostgresPort(), + }) + + DB.AddQueryHook(dbLogger{}) + +} diff --git a/pkg/database/init.go b/pkg/database/init.go new file mode 100644 index 0000000..1949e27 --- /dev/null +++ b/pkg/database/init.go @@ -0,0 +1,23 @@ +// Contains utility functions around the database + +package database + +import ( + "glsamaker/pkg/database/connection" + "glsamaker/pkg/database/schema" + "glsamaker/pkg/logger" + "log" +) + +// Connect is used to connect to the database +// and turn on logging if desired +func Connect() { + connection.Connect() + err := schema.CreateSchema(connection.DB) + if err != nil { + logger.Error.Println("ERROR: Could not create database schema") + logger.Error.Println(err) + log.Fatalln(err) + } + +} diff --git a/pkg/database/schema/create.go b/pkg/database/schema/create.go new file mode 100644 index 0000000..d87d962 --- /dev/null +++ b/pkg/database/schema/create.go @@ -0,0 +1,36 @@ +package schema + +import ( + "glsamaker/pkg/models" + "glsamaker/pkg/models/bugzilla" + "glsamaker/pkg/models/cve" + "glsamaker/pkg/models/users" + "github.com/go-pg/pg/v9" + "github.com/go-pg/pg/v9/orm" +) + +// CreateSchema creates the tables in the database +// in case they don't alreay exist +func CreateSchema(dbCon *pg.DB) error { + for _, model := range []interface{}{ + (*models.GlobalSettings)(nil), + (*models.ApplicationSetting)(nil), + (*users.User)(nil), + (*models.Session)(nil), + (*bugzilla.Bug)(nil), + (*models.Glsa)(nil), + (*models.GlsaToBug)(nil), + (*cve.Comment)(nil), + (*cve.DefCveItem)(nil), + (*cve.DefCveItemToBug)(nil)} { + + err := dbCon.CreateTable(model, &orm.CreateTableOptions{ + IfNotExists: true, + }) + if err != nil { + return err + } + + } + return nil +} diff --git a/pkg/logger/loggers.go b/pkg/logger/loggers.go new file mode 100644 index 0000000..0ef51e9 --- /dev/null +++ b/pkg/logger/loggers.go @@ -0,0 +1,39 @@ +package logger + +import ( + "io" + "log" + "os" +) + +var ( + Debug *log.Logger + Info *log.Logger + Error *log.Logger +) + +func Init( + debugHandle io.Writer, + infoHandle io.Writer, + errorHandle io.Writer) { + + Debug = log.New(debugHandle, + "DEBUG: ", + log.Ldate|log.Ltime|log.Lshortfile) + + Info = log.New(infoHandle, + "INFO: ", + log.Ldate|log.Ltime|log.Lshortfile) + + Error = log.New(errorHandle, + "ERROR: ", + log.Ldate|log.Ltime|log.Lshortfile) +} + +func CreateLogFile(path string) *os.File { + f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Println(err) + } + return f +} diff --git a/pkg/models/application.go b/pkg/models/application.go new file mode 100644 index 0000000..56e673c --- /dev/null +++ b/pkg/models/application.go @@ -0,0 +1,69 @@ +// Contains the model of the application data + +package models + +import ( + "glsamaker/pkg/config" + "glsamaker/pkg/database/connection" + "time" +) + +type ApplicationSetting struct { + Key string `pg:",pk"` + Value string + LastUpdate time.Time + //LastBugUpdate time.Time + //LastCVEUpdate time.Time +} + +type GlobalSettings struct { + LastBugUpdate time.Time + LastCVEUpdate time.Time + Version string + Force2FALogin bool + Force2FAGLSARelease bool +} + +func GetApplicationKey(key string) *ApplicationSetting { + applicationData := &ApplicationSetting{Key: key} + connection.DB.Model(applicationData).WherePK().Select() + return applicationData +} + +func SetApplicationValue(key string, value string) { + applicationData := &ApplicationSetting{ + Key: key, + Value: value, + LastUpdate: time.Now(), + } + + connection.DB.Model(applicationData).WherePK().OnConflict("(key) DO Update").Insert() +} + +func SeedApplicationValue(key string, value string) { + applicationData := &ApplicationSetting{ + Key: key, + Value: value, + LastUpdate: time.Now(), + } + + connection.DB.Model(applicationData).WherePK().OnConflict("(key) DO Nothing").Insert() +} + +func GetDefaultGlobalSettings() *GlobalSettings { + return &GlobalSettings{ + LastBugUpdate: GetApplicationKey("LastBugUpdate").LastUpdate, + LastCVEUpdate: GetApplicationKey("LastCVEUpdate").LastUpdate, + Version: GetApplicationKey("Version").Value, + Force2FALogin: GetApplicationKey("Force2FALogin").Value == "1", + Force2FAGLSARelease: GetApplicationKey("Force2FAGLSARelease").Value == "1", + } +} + +func SeedInitialApplicationData() { + SeedApplicationValue("LastBugUpdate", "") + SeedApplicationValue("LastCVEUpdate", "") + SeedApplicationValue("Version", config.Version()) + SeedApplicationValue("Force2FALogin", "0") + SeedApplicationValue("Force2FAGLSARelease", "0") +} diff --git a/pkg/models/bugzilla/bugs.go b/pkg/models/bugzilla/bugs.go new file mode 100644 index 0000000..9654841 --- /dev/null +++ b/pkg/models/bugzilla/bugs.go @@ -0,0 +1,156 @@ +package bugzilla + +import ( + "encoding/json" + "glsamaker/pkg/database/connection" + "io/ioutil" + "net/http" + "strconv" + "strings" +) + +type Bugs struct { + Bugs []Bug +} + +// missing flags +type Bug struct { + Id int64 `json:"id" pg:",pk"` + Alias []string `json:"alias"` + AssignedTo string `json:"assigned_to"` + AssignedToDetail Contributor `json:"assigned_to"` + Blocks []int64 `json:"blocks"` + CC []string `json:"cc"` + CCDetail []Contributor `json:"cc_detail"` + Classification string `json:"classification"` + Component string `json:"component"` + CreationTime string `json:"creation_time"` + Creator string `json:"creator"` + CreatorDetail Contributor `json:"creator_detail"` + DependsOn []int64 `json:"depends_on"` + DupeOf int64 `json:"dupe_of"` + Groups []string `json:"groups"` + IsCCAccessible bool `json:"is_cc_accessible"` + IsConfirmed bool `json:"is_confirmed"` + IsCreatorAccessible bool `json:"is_creator_accessible"` + IsOpen bool `json:"is_open"` + Keywords []string `json:"keywords"` + LastChangeTime string `json:"last_change_time"` + OpSys string `json:"op_sys"` + Platform string `json:"platform"` + Priority string `json:"priority"` + Product string `json:"product"` + QAContact string `json:"qa_contact"` + Resolution string `json:"resolution"` + SeeAlso []string `json:"see_also"` + Severity string `json:"severity"` + Status string `json:"status"` + Summary string `json:"summary"` + TargetMilestone string `json:"target_milestone"` + Url string `json:"url"` + Version string `json:"version"` + Whiteboard string `json:"whiteboard"` +} + +type Contributor struct { + Email string `json:"email"` + Id int64 `json:"id"` + Name string `json:"name"` + RealName string `json:"real_name"` +} + +func (bug *Bug) IsReady() bool { + return strings.Contains(bug.Whiteboard, "[glsa") +} + +func GetBugById(id string) Bug { + + parsedId, err := strconv.ParseInt(id, 10, 64) + if err != nil { + return Bug{} + } + + bug := &Bug{Id: parsedId} + err = connection.DB.Model(bug).WherePK().Select() + + if err == nil && bug != nil { + return *bug + } + + resp, err := http.Get("https://bugs.gentoo.org/rest/bug?id=" + id) + if err != nil { + return Bug{} + } + + // Read body + b, err := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + if err != nil { + return Bug{} + } + + // Unmarshal + var bugs Bugs + err = json.Unmarshal(b, &bugs) + if err != nil || bugs.Bugs == nil || len(bugs.Bugs) == 0 { + return Bug{} + } + + return bugs.Bugs[0] +} + +func GetBugsByIds(ids []string) []Bug { + + var result []Bug + + if len(ids) < 1 { + return result + } + + // get existing bugs from database + var newBugIds []string + for _, id := range ids { + + parsedId, err := strconv.ParseInt(id, 10, 64) + if err != nil { + continue + } + + bug := &Bug{Id: parsedId} + err = connection.DB.Model(bug).WherePK().Select() + + if err == nil && bug != nil { + result = append(result, *bug) + } else { + newBugIds = append(newBugIds, id) + } + + } + + // if there are new bugs, import them + if len(newBugIds) > 0 { + var bugs Bugs + resp, err := http.Get("https://bugs.gentoo.org/rest/bug?id=" + strings.Join(newBugIds, ",")) + + if err != nil { + return bugs.Bugs + } + + // Read body + b, err := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + if err != nil { + return bugs.Bugs + } + + // Unmarshal + err = json.Unmarshal(b, &bugs) + if err != nil || bugs.Bugs == nil || len(bugs.Bugs) == 0 { + return result + } + + result = append(result, bugs.Bugs...) + } + + return result +} diff --git a/pkg/models/cve/cve.go b/pkg/models/cve/cve.go new file mode 100644 index 0000000..397b156 --- /dev/null +++ b/pkg/models/cve/cve.go @@ -0,0 +1,86 @@ +package cve + +// CVE +type CVE struct { + Affects *Affects `json:"affects,omitempty"` + CVEDataMeta *CVEDataMeta `json:"CVE_data_meta"` + DataFormat string `json:"data_format"` + DataType string `json:"data_type"` + DataVersion string `json:"data_version"` + Description *Description `json:"description"` + Problemtype *Problemtype `json:"problemtype"` + References *References `json:"references"` +} + +// Affects +type Affects struct { + Vendor *Vendor `json:"vendor"` +} + +// CVEDataMeta +type CVEDataMeta struct { + ASSIGNER string `json:"ASSIGNER"` + ID string `json:"ID"` + STATE string `json:"STATE,omitempty"` +} + +// Description +type Description struct { + DescriptionData []*LangString `json:"description_data"` +} + +// LangString +type LangString struct { + Lang string `json:"lang"` + Value string `json:"value"` +} + +// Problemtype +type Problemtype struct { + ProblemtypeData []*ProblemtypeDataItems `json:"problemtype_data"` +} + +// ProblemtypeDataItems +type ProblemtypeDataItems struct { + Description []*LangString `json:"description"` +} + +// Product +type Product struct { + ProductData []*Product `json:"product_data"` +} + +// Reference +type Reference struct { + Name string `json:"name,omitempty"` + Refsource string `json:"refsource,omitempty"` + Tags []string `json:"tags,omitempty"` + Url string `json:"url"` +} + +// References +type References struct { + ReferenceData []*Reference `json:"reference_data"` +} + +// Vendor +type Vendor struct { + VendorData []*VendorDataItems `json:"vendor_data"` +} + +// VendorDataItems +type VendorDataItems struct { + Product *Product `json:"product"` + VendorName string `json:"vendor_name"` +} + +// Version +type Version struct { + VersionData []*VersionDataItems `json:"version_data"` +} + +// VersionDataItems +type VersionDataItems struct { + VersionAffected string `json:"version_affected,omitempty"` + VersionValue string `json:"version_value"` +} diff --git a/pkg/models/cve/cvss.go b/pkg/models/cve/cvss.go new file mode 100644 index 0000000..987a5d5 --- /dev/null +++ b/pkg/models/cve/cvss.go @@ -0,0 +1,62 @@ +package cve + +// CvssV2 Common Vulnerability Scoring System version 2.0 +type CvssV2 struct { + AccessComplexity string `json:"accessComplexity,omitempty"` + AccessVector string `json:"accessVector,omitempty"` + Authentication string `json:"authentication,omitempty"` + AvailabilityImpact string `json:"availabilityImpact,omitempty"` + AvailabilityRequirement string `json:"availabilityRequirement,omitempty"` + BaseScore float64 `json:"baseScore"` + CollateralDamagePotential string `json:"collateralDamagePotential,omitempty"` + ConfidentialityImpact string `json:"confidentialityImpact,omitempty"` + ConfidentialityRequirement string `json:"confidentialityRequirement,omitempty"` + EnvironmentalScore float64 `json:"environmentalScore,omitempty"` + Exploitability string `json:"exploitability,omitempty"` + IntegrityImpact string `json:"integrityImpact,omitempty"` + IntegrityRequirement string `json:"integrityRequirement,omitempty"` + RemediationLevel string `json:"remediationLevel,omitempty"` + ReportConfidence string `json:"reportConfidence,omitempty"` + TargetDistribution string `json:"targetDistribution,omitempty"` + TemporalScore float64 `json:"temporalScore,omitempty"` + VectorString string `json:"vectorString"` + + // CVSS Version + Version string `json:"version"` +} + +// CvssV3 Common Vulnerability Scoring System version 3.x (BETA) +type CvssV3 struct { + AttackComplexity string `json:"attackComplexity,omitempty"` + AttackVector string `json:"attackVector,omitempty"` + AvailabilityImpact string `json:"availabilityImpact,omitempty"` + AvailabilityRequirement string `json:"availabilityRequirement,omitempty"` + BaseScore float64 `json:"baseScore"` + BaseSeverity string `json:"baseSeverity"` + ConfidentialityImpact string `json:"confidentialityImpact,omitempty"` + ConfidentialityRequirement string `json:"confidentialityRequirement,omitempty"` + EnvironmentalScore float64 `json:"environmentalScore,omitempty"` + EnvironmentalSeverity string `json:"environmentalSeverity,omitempty"` + ExploitCodeMaturity string `json:"exploitCodeMaturity,omitempty"` + IntegrityImpact string `json:"integrityImpact,omitempty"` + IntegrityRequirement string `json:"integrityRequirement,omitempty"` + ModifiedAttackComplexity string `json:"modifiedAttackComplexity,omitempty"` + ModifiedAttackVector string `json:"modifiedAttackVector,omitempty"` + ModifiedAvailabilityImpact string `json:"modifiedAvailabilityImpact,omitempty"` + ModifiedConfidentialityImpact string `json:"modifiedConfidentialityImpact,omitempty"` + ModifiedIntegrityImpact string `json:"modifiedIntegrityImpact,omitempty"` + ModifiedPrivilegesRequired string `json:"modifiedPrivilegesRequired,omitempty"` + ModifiedScope string `json:"modifiedScope,omitempty"` + ModifiedUserInteraction string `json:"modifiedUserInteraction,omitempty"` + PrivilegesRequired string `json:"privilegesRequired,omitempty"` + RemediationLevel string `json:"remediationLevel,omitempty"` + ReportConfidence string `json:"reportConfidence,omitempty"` + Scope string `json:"scope,omitempty"` + TemporalScore float64 `json:"temporalScore,omitempty"` + TemporalSeverity string `json:"temporalSeverity,omitempty"` + UserInteraction string `json:"userInteraction,omitempty"` + VectorString string `json:"vectorString"` + + // CVSS Version + Version string `json:"version"` +} diff --git a/pkg/models/cve/feed.go b/pkg/models/cve/feed.go new file mode 100644 index 0000000..527f233 --- /dev/null +++ b/pkg/models/cve/feed.go @@ -0,0 +1,116 @@ +package cve + +import ( + "glsamaker/pkg/models/bugzilla" + "glsamaker/pkg/models/gpackage" + "glsamaker/pkg/models/users" + "time" +) + +// NVDFeed +type NVDFeed struct { + CVEDataFormat string `json:"CVE_data_format"` + + // NVD adds number of CVE in this feed + CVEDataNumberOfCVEs string `json:"CVE_data_numberOfCVEs,omitempty"` + + // NVD adds feed date timestamp + CVEDataTimestamp string `json:"CVE_data_timestamp,omitempty"` + CVEDataType string `json:"CVE_data_type"` + CVEDataVersion string `json:"CVE_data_version"` + + // NVD feed array of CVE + CVEItems []*DefCveItem `json:"CVE_Items"` +} + +// DefConfigurations Defines the set of product configurations for a NVD applicability statement. +type DefConfigurations struct { + CVEDataVersion string `json:"CVE_data_version"` + Nodes []*DefNode `json:"nodes,omitempty"` +} + +// DefCpeMatch CPE match string or range +type DefCpeMatch struct { + Cpe22Uri string `json:"cpe22Uri,omitempty"` + Cpe23Uri string `json:"cpe23Uri"` + CpeName []*DefCpeName `json:"cpe_name,omitempty"` + VersionEndExcluding string `json:"versionEndExcluding,omitempty"` + VersionEndIncluding string `json:"versionEndIncluding,omitempty"` + VersionStartExcluding string `json:"versionStartExcluding,omitempty"` + VersionStartIncluding string `json:"versionStartIncluding,omitempty"` + Vulnerable bool `json:"vulnerable"` +} + +// DefCpeName CPE name +type DefCpeName struct { + Cpe22Uri string `json:"cpe22Uri,omitempty"` + Cpe23Uri string `json:"cpe23Uri"` + LastModifiedDate string `json:"lastModifiedDate,omitempty"` +} + +// DefCveItem Defines a vulnerability in the NVD data feed. +type DefCveItem struct { + Id string `pg:",pk"` + State string `pg:"state"` + Configurations *DefConfigurations `json:"configurations,omitempty"` + Cve CVE `json:"cve"` + Description string + Impact *DefImpact `json:"impact,omitempty"` + LastModifiedDate string `json:"lastModifiedDate,omitempty"` + PublishedDate string `json:"publishedDate,omitempty"` + + Comments []Comment `pg:",fk:cve_id"` + Packages []gpackage.Package + Bugs []bugzilla.Bug `pg:"many2many:def_cve_item_to_bugs,joinFK:bug_id"` +} + +type DefCveItemToBug struct { + DefCveItemId string `pg:",unique:cve_to_bug"` + BugId int64 `pg:",unique:cve_to_bug"` +} + +type Comment struct { + Id int64 `pg:",pk,unique"` + GlsaId int64 + CVEId string + User int64 + UserBadge users.Badge + Type string + Message string + // Date time.Time `pg:"-"` + Date time.Time +} + +// DefNode Defines a node or sub-node in an NVD applicability statement. +type DefNode struct { + Children []*DefNode `json:"children,omitempty"` + CpeMatch []*DefCpeMatch `json:"cpe_match,omitempty"` + Negate bool `json:"negate,omitempty"` + Operator string `json:"operator,omitempty"` +} + +// DefImpact Impact scores for a vulnerability as found on NVD. +type DefImpact struct { + BaseMetricV3 BaseMetricV3 `json:"baseMetricV3"` + BaseMetricV2 BaseMetricV2 `json:"baseMetricV2"` +} + +// BaseMetricV2 CVSS V2.0 score. +type BaseMetricV2 struct { + CvssV2 CvssV2 `json:"cvssV2"` + Severity string `json:"severity"` + ExploitabilityScore float32 `json:"exploitabilityScore"` + ImpactScore float32 `json:"impactScore"` + AcInsufInfo bool `json:"acInsufInfo"` + ObtainAllPrivilege bool `json:"obtainAllPrivilege"` + ObtainUserPrivilege bool `json:"obtainUserPrivilege"` + ObtainOtherPrivilege bool `json:"obtainOtherPrivilege"` + UserInteractionRequired bool `json:"userInteractionRequired"` +} + +// BaseMetricV3 CVSS V3.x score. +type BaseMetricV3 struct { + CvssV3 CvssV3 `json:"cvssV3"` + ExploitabilityScore float32 `json:"exploitabilityScore"` + ImpactScore float32 `json:"impactScore"` +} diff --git a/pkg/models/glsa.go b/pkg/models/glsa.go new file mode 100644 index 0000000..9f8c3ec --- /dev/null +++ b/pkg/models/glsa.go @@ -0,0 +1,115 @@ +package models + +import ( + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models/bugzilla" + "glsamaker/pkg/models/cve" + "glsamaker/pkg/models/gpackage" + "glsamaker/pkg/models/users" + "time" +) + +type Glsa struct { + // Id string + Id int64 `pg:",pk,unique"` + Alias string + Type string + Title string + Synopsis string + Packages []gpackage.Package + Description string + Impact string + Workaround string + Resolution string + References []Reference + Permission string + Access string + Severity string + Keyword string + Background string + Bugs []bugzilla.Bug `pg:"many2many:glsa_to_bugs,joinFK:bug_id"` + Comments []cve.Comment `pg:",fk:glsa_id"` + Revision string + ApprovedBy []int64 + DeclinedBy []int64 + CreatorId int64 + Creator *users.User + Created time.Time + Updated time.Time + Status Status `pg:"-"` +} + +type GlsaToBug struct { + GlsaId int64 `pg:",unique:glsa_to_bug"` + BugId int64 `pg:",unique:glsa_to_bug"` +} + +type Reference struct { + Title string + URL string +} + +type Status struct { + BugReady bool + Approval string + WorkflowStatus string + Permission string +} + +func (glsa *Glsa) IsBugReady() bool { + bugReady := true + for _, bug := range glsa.Bugs { + bugReady = bugReady && bug.IsReady() + } + return bugReady +} + +func (glsa *Glsa) ComputeStatus(user *users.User) { + status := Status{ + BugReady: glsa.IsBugReady(), + Approval: "none", + WorkflowStatus: "todo", + Permission: glsa.Permission, + } + + if glsa.DeclinedBy != nil && len(glsa.DeclinedBy) > 0 { + status.Approval = "declined" + } else if glsa.ApprovedBy != nil && len(glsa.ApprovedBy) > 0 { + status.Approval = "approved" + } else if glsa.Comments != nil && len(glsa.Comments) > 0 { + status.Approval = "comments" + } + + if glsa.CreatorId == user.Id { + status.WorkflowStatus = "own" + } else if contains(glsa.ApprovedBy, user.Id) { + status.WorkflowStatus = "approved" + } else { + for _, comment := range glsa.Comments { + if comment.User == user.Id { + status.WorkflowStatus = "commented" + break + } + } + } + + glsa.Status = status +} + +func (glsa *Glsa) ComputeCommentBadges() { + for _, comment := range glsa.Comments { + user := new(users.User) + connection.DB.Model(user).Where("id = ?", comment.User).Select() + + comment.UserBadge = user.Badge + } +} + +func contains(arr []int64, element int64) bool { + for _, a := range arr { + if a == element { + return true + } + } + return false +} diff --git a/pkg/models/gpackage/package.go b/pkg/models/gpackage/package.go new file mode 100644 index 0000000..76166b7 --- /dev/null +++ b/pkg/models/gpackage/package.go @@ -0,0 +1,11 @@ +package gpackage + +type Package struct { + Affected bool + Atom string + Identifier string + Version string + Slot string + Arch string + Auto bool +} diff --git a/pkg/models/session.go b/pkg/models/session.go new file mode 100644 index 0000000..b83da06 --- /dev/null +++ b/pkg/models/session.go @@ -0,0 +1,15 @@ +package models + +import ( + "glsamaker/pkg/models/users" + "time" +) + +type Session struct { + Id string `pg:",pk"` + UserId int64 + User *users.User + SecondFactorMissing bool + IP string + Expires time.Time +} diff --git a/pkg/models/users/user.go b/pkg/models/users/user.go new file mode 100644 index 0000000..b8a60d6 --- /dev/null +++ b/pkg/models/users/user.go @@ -0,0 +1,214 @@ +// Contains the model of the application data + +package users + +import ( + "crypto/rand" + "errors" + "github.com/duo-labs/webauthn/protocol" + "github.com/duo-labs/webauthn/webauthn" + "github.com/go-pg/pg/v9/orm" + "golang.org/x/crypto/argon2" + "strconv" + "strings" +) + +type User struct { + Id int64 `pg:",pk,unique"` + + Email string `pg:",unique"` + Nick string + Name string + Password Argon2Parameters + ForcePasswordChange bool + Role string + TOTPSecret string + TOTPQRCode string + IsUsingTOTP bool + WebauthnCredentials []webauthn.Credential + WebauthnCredentialNames []*WebauthnCredentialName + IsUsingWebAuthn bool + Show2FANotice bool + + Permissions Permissions + + Badge Badge + + ForcePasswordRotation bool + Force2FA bool + + Disabled bool +} + +type Argon2Parameters struct { + Type string + Salt []byte + Time uint32 + Memory uint32 + Threads uint8 + KeyLen uint32 + Hash []byte +} + +func (a *Argon2Parameters) GenerateSalt(n uint32) error { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return err + } + a.Salt = b + + return nil +} + +func (a *Argon2Parameters) GeneratePassword(password string) error { + if a.Salt == nil || a.Time == 0 || a.Memory == 0 || a.Threads == 0 || a.KeyLen == 0 { + return errors.New("Invalid parameters") + } + a.Hash = argon2.IDKey([]byte(password), a.Salt, a.Time, a.Memory, a.Threads, a.KeyLen) + return nil +} + +func (u *User) UpdatePassword(password string) error { + err := u.Password.GeneratePassword(password) + if err != nil { + return err + } + return nil +} + +func (u *User) CheckPassword(password string) bool { + return string(u.Password.Hash) == string(argon2.IDKey( + []byte(password), + u.Password.Salt, + u.Password.Time, + u.Password.Memory, + u.Password.Threads, + u.Password.KeyLen)) +} + +type Permissions struct { + Glsa GlsaPermissions + CVETool CVEToolPermissions + Admin AdminPermissions +} + +type GlsaPermissions struct { + View bool + UpdateBugs bool + Comment bool + Create bool + Edit bool + Approve bool + ApproveOwnGlsa bool + Decline bool + Delete bool + Release bool + Confidential bool +} + +type CVEToolPermissions struct { + View bool + UpdateCVEs bool + Comment bool + AddPackage bool + ChangeState bool + AssignBug bool + CreateBug bool +} + +type AdminPermissions struct { + View bool + CreateTemplates bool + ManageUsers bool + GlobalSettings bool +} + +type WebauthnCredentialName struct { + Id []byte + Name string +} + +type Badge struct { + Name string `pg:",pk"` + Description string + Color string +} + +func (u User) IsUsing2FA() bool { + return u.IsUsingTOTP || u.IsUsingWebAuthn +} + +// WebAuthnID returns the user's ID +func (u User) WebAuthnID() []byte { + return []byte(strconv.FormatInt(u.Id, 10)) +} + +// WebAuthnName returns the user's username +func (u User) WebAuthnName() string { + return strings.TrimRight(u.Nick, "@") +} + +// WebAuthnDisplayName returns the user's display name +func (u User) WebAuthnDisplayName() string { + return strings.TrimRight(u.Nick, "@") +} + +// WebAuthnIcon is not (yet) implemented +func (u User) WebAuthnIcon() string { + return "" +} + +// WebAuthnCredentials returns credentials owned by the user +func (u User) WebAuthnCredentials() []webauthn.Credential { + return u.WebauthnCredentials +} + +// CredentialExcludeList returns a CredentialDescriptor array filled +// with all the user's credentials +func (u User) CredentialExcludeList() []protocol.CredentialDescriptor { + + credentialExcludeList := []protocol.CredentialDescriptor{} + for _, cred := range u.WebauthnCredentials { + descriptor := protocol.CredentialDescriptor{ + Type: protocol.PublicKeyCredentialType, + CredentialID: cred.ID, + } + credentialExcludeList = append(credentialExcludeList, descriptor) + } + + return credentialExcludeList +} + +// AddCredential associates the credential to the user +func (u *User) AddCredential(cred webauthn.Credential, credentialName string) { + u.WebauthnCredentials = append(u.WebauthnCredentials, cred) + + webauthnCredentialName := &WebauthnCredentialName{ + Id: cred.ID, + Name: credentialName, + } + + u.WebauthnCredentialNames = append(u.WebauthnCredentialNames, webauthnCredentialName) + +} + +func (u *User) CanEditCVEs() bool { + return u.Role == "admin" || u.Role == "editor" +} + +func (u *User) Confidential() string { + confidential := "public" + if u.Permissions.Glsa.Confidential { + confidential = "confidential" + } + return confidential +} + +func (u *User) CanAccess(query *orm.Query) *orm.Query { + return query.WhereGroup(func(q *orm.Query) (*orm.Query, error) { + q = q.WhereOr("permission = ?", "public"). + WhereOr("permission = ?", u.Confidential()) + return q, nil + }) +} |