summaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
Diffstat (limited to 'pkg')
-rw-r--r--pkg/app/handler/about/index.go28
-rw-r--r--pkg/app/handler/about/utils.go58
-rw-r--r--pkg/app/handler/account/password.go109
-rw-r--r--pkg/app/handler/account/twofactor.go200
-rw-r--r--pkg/app/handler/admin/edit.go293
-rw-r--r--pkg/app/handler/admin/index.go27
-rw-r--r--pkg/app/handler/admin/passwordreset.go73
-rw-r--r--pkg/app/handler/admin/utils.go112
-rw-r--r--pkg/app/handler/all/index.go40
-rw-r--r--pkg/app/handler/all/utils.go36
-rw-r--r--pkg/app/handler/archive/index.go44
-rw-r--r--pkg/app/handler/archive/utils.go36
-rw-r--r--pkg/app/handler/authentication/accessDenied.go10
-rw-r--r--pkg/app/handler/authentication/auth_session/authsession.go177
-rw-r--r--pkg/app/handler/authentication/login.go100
-rw-r--r--pkg/app/handler/authentication/logout.go29
-rw-r--r--pkg/app/handler/authentication/templates/admin.go36
-rw-r--r--pkg/app/handler/authentication/templates/login.go32
-rw-r--r--pkg/app/handler/authentication/templates/totp.go23
-rw-r--r--pkg/app/handler/authentication/templates/webauthn.go23
-rw-r--r--pkg/app/handler/authentication/totp/totp.go60
-rw-r--r--pkg/app/handler/authentication/utils/utils.go81
-rw-r--r--pkg/app/handler/authentication/webauthn/login.go118
-rw-r--r--pkg/app/handler/authentication/webauthn/register.go111
-rw-r--r--pkg/app/handler/cvetool/bug.go85
-rw-r--r--pkg/app/handler/cvetool/comments.go74
-rw-r--r--pkg/app/handler/cvetool/index.go169
-rw-r--r--pkg/app/handler/cvetool/state.go75
-rw-r--r--pkg/app/handler/cvetool/update.go23
-rw-r--r--pkg/app/handler/cvetool/utils.go47
-rw-r--r--pkg/app/handler/dashboard/index.go58
-rw-r--r--pkg/app/handler/dashboard/utils.go44
-rw-r--r--pkg/app/handler/drafts/index.go44
-rw-r--r--pkg/app/handler/drafts/utils.go37
-rw-r--r--pkg/app/handler/glsa/bugs.go63
-rw-r--r--pkg/app/handler/glsa/comments.go110
-rw-r--r--pkg/app/handler/glsa/delete.go42
-rw-r--r--pkg/app/handler/glsa/edit.go185
-rw-r--r--pkg/app/handler/glsa/release.go70
-rw-r--r--pkg/app/handler/glsa/utils.go79
-rw-r--r--pkg/app/handler/glsa/view.go48
-rw-r--r--pkg/app/handler/home/index.go16
-rw-r--r--pkg/app/handler/home/utils.go34
-rw-r--r--pkg/app/handler/newRequest/index.go186
-rw-r--r--pkg/app/handler/newRequest/utils.go36
-rw-r--r--pkg/app/handler/requests/index.go44
-rw-r--r--pkg/app/handler/requests/utils.go36
-rw-r--r--pkg/app/handler/search/index.go126
-rw-r--r--pkg/app/handler/search/utils.go38
-rw-r--r--pkg/app/handler/statistics/index.go42
-rw-r--r--pkg/app/handler/statistics/utils.go48
-rw-r--r--pkg/app/serve.go214
-rw-r--r--pkg/app/utils.go89
-rw-r--r--pkg/config/config.go63
-rw-r--r--pkg/cveimport/update.go96
-rw-r--r--pkg/database/connection/connection.go42
-rw-r--r--pkg/database/init.go23
-rw-r--r--pkg/database/schema/create.go36
-rw-r--r--pkg/logger/loggers.go39
-rw-r--r--pkg/models/application.go69
-rw-r--r--pkg/models/bugzilla/bugs.go156
-rw-r--r--pkg/models/cve/cve.go86
-rw-r--r--pkg/models/cve/cvss.go62
-rw-r--r--pkg/models/cve/feed.go116
-rw-r--r--pkg/models/glsa.go115
-rw-r--r--pkg/models/gpackage/package.go11
-rw-r--r--pkg/models/session.go15
-rw-r--r--pkg/models/users/user.go214
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
+ })
+}