mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-25 21:20:50 +02:00
@@ -33,6 +33,7 @@ var AttachmentColumnsMap = map[string]string{
|
||||
"name": repository.TableColumn(database.ATTACHMENT_TABLE, "name"),
|
||||
"description": repository.TableColumn(database.ATTACHMENT_TABLE, "description"),
|
||||
"embedded content": repository.TableColumn(database.ATTACHMENT_TABLE, "embeddedContent"),
|
||||
"send as calendar": repository.TableColumn(database.ATTACHMENT_TABLE, "sendAsCalendar"),
|
||||
"filename": repository.TableColumn(database.ATTACHMENT_TABLE, "filename"),
|
||||
}
|
||||
|
||||
@@ -255,6 +256,11 @@ func (a *Attachment) Create(g *gin.Context) {
|
||||
if strings.ToLower(embeddedContentString) == "true" {
|
||||
embeddedContent.Set(true)
|
||||
}
|
||||
sendAsCalendar := nullable.NewNullableWithValue(false)
|
||||
sendAsCalendarString := g.PostForm("sendAsCalendar")
|
||||
if strings.ToLower(sendAsCalendarString) == "true" {
|
||||
sendAsCalendar.Set(true)
|
||||
}
|
||||
attachments := []*model.Attachment{}
|
||||
for _, file := range multipartData.File["files"] {
|
||||
// TODO multi user validate that the company id is the same as the session company id or that the session is a super admin
|
||||
@@ -297,6 +303,7 @@ func (a *Attachment) Create(g *gin.Context) {
|
||||
Name: name,
|
||||
Description: description,
|
||||
EmbeddedContent: embeddedContent,
|
||||
SendAsCalendar: sendAsCalendar,
|
||||
File: file,
|
||||
FileName: fileName,
|
||||
}
|
||||
|
||||
@@ -1228,3 +1228,4 @@ func (c *Campaign) UploadReportedCSV(g *gin.Context) {
|
||||
"message": "CSV processed successfully",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,175 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/remotebrowser"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
// ReportTemplate is the report template controller
|
||||
type ReportTemplate struct {
|
||||
Common
|
||||
ReportTemplateService *service.ReportTemplate
|
||||
CampaignService *service.Campaign
|
||||
OptionService *service.Option
|
||||
ExecPath string
|
||||
}
|
||||
|
||||
// Create creates a report template
|
||||
func (r *ReportTemplate) Create(g *gin.Context) {
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req model.ReportTemplate
|
||||
if ok := r.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
id, err := r.ReportTemplateService.Create(g.Request.Context(), session, &req)
|
||||
if ok := r.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
r.Response.OK(g, gin.H{"id": id.String()})
|
||||
}
|
||||
|
||||
// GetAll gets report templates
|
||||
func (r *ReportTemplate) GetAll(g *gin.Context) {
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs, ok := r.handleQueryArgs(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
queryArgs.DefaultSortByUpdatedAt()
|
||||
companyID := companyIDFromRequestQuery(g)
|
||||
result, err := r.ReportTemplateService.GetAll(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
companyID,
|
||||
&repository.ReportTemplateOption{QueryArgs: queryArgs},
|
||||
)
|
||||
if ok := r.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
r.Response.OK(g, result)
|
||||
}
|
||||
|
||||
// GetByID gets a report template by id
|
||||
func (r *ReportTemplate) GetByID(g *gin.Context) {
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
id, ok := r.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
tmpl, err := r.ReportTemplateService.GetByID(g.Request.Context(), session, id)
|
||||
if ok := r.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
r.Response.OK(g, tmpl)
|
||||
}
|
||||
|
||||
// UpdateByID updates a report template
|
||||
func (r *ReportTemplate) UpdateByID(g *gin.Context) {
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
id, ok := r.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req model.ReportTemplate
|
||||
if ok := r.handleParseRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
err := r.ReportTemplateService.UpdateByID(g.Request.Context(), session, id, &req)
|
||||
if ok := r.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
r.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// DeleteByID deletes a report template
|
||||
func (r *ReportTemplate) DeleteByID(g *gin.Context) {
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
id, ok := r.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
err := r.ReportTemplateService.DeleteByID(g.Request.Context(), session, id)
|
||||
if ok := r.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
r.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// GeneratePDFByCampaignID generates a PDF report for a campaign
|
||||
func (r *ReportTemplate) GeneratePDFByCampaignID(g *gin.Context) {
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
opt, _ := r.OptionService.GetOptionWithoutAuth(g.Request.Context(), data.OptionKeyReportPDFEnabled)
|
||||
if opt == nil || opt.Value.String() != "true" {
|
||||
r.Response.NotFound(g)
|
||||
return
|
||||
}
|
||||
id, ok := r.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
htmlContent, campaignName, err := r.CampaignService.BuildReportHTML(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
)
|
||||
if ok := r.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
pdfBytes, err := remotebrowser.RenderHTMLToPDF(g.Request.Context(), htmlContent, r.ExecPath)
|
||||
if ok := r.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
filename := fmt.Sprintf("report-%s.pdf", sanitizeFilename(campaignName))
|
||||
r.responseWithPDF(g, pdfBytes, filename)
|
||||
}
|
||||
|
||||
// WipeBrowserCache deletes the auto-downloaded Chromium binary used for PDF generation.
|
||||
func (r *ReportTemplate) WipeBrowserCache(g *gin.Context) {
|
||||
session, _, ok := r.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := r.ReportTemplateService.WipeBrowserCache(session); err != nil {
|
||||
r.handleErrors(g, err)
|
||||
return
|
||||
}
|
||||
r.Response.OK(g, gin.H{})
|
||||
}
|
||||
|
||||
// sanitizeFilename removes characters that are unsafe in filenames
|
||||
func sanitizeFilename(name string) string {
|
||||
replacer := strings.NewReplacer(
|
||||
"/", "-", "\\", "-", ":", "-", "*", "-",
|
||||
"?", "-", "\"", "-", "<", "-", ">", "-", "|", "-",
|
||||
)
|
||||
safe := strings.TrimSpace(replacer.Replace(name))
|
||||
if safe == "" {
|
||||
return "campaign"
|
||||
}
|
||||
return safe
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
@@ -11,6 +15,8 @@ import (
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
const ssoStateCookieKey = "sso_state"
|
||||
|
||||
// SSO the single sign on controller
|
||||
type SSO struct {
|
||||
Common
|
||||
@@ -60,10 +66,45 @@ func (s *SSO) EntreIDLogin(g *gin.Context) {
|
||||
s.Response.BadRequest(g)
|
||||
return
|
||||
}
|
||||
g.Redirect(http.StatusTemporaryRedirect, authURL)
|
||||
|
||||
stateBytes := make([]byte, 32)
|
||||
if _, err := rand.Read(stateBytes); err != nil {
|
||||
s.Response.ServerError(g)
|
||||
return
|
||||
}
|
||||
state := hex.EncodeToString(stateBytes)
|
||||
|
||||
http.SetCookie(g.Writer, &http.Cookie{
|
||||
Name: ssoStateCookieKey,
|
||||
Value: state,
|
||||
Path: "/",
|
||||
MaxAge: int(5 * time.Minute / time.Second),
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
g.Redirect(http.StatusTemporaryRedirect, authURL+"&state="+state)
|
||||
}
|
||||
|
||||
func (s *SSO) EntreIDCallBack(g *gin.Context) {
|
||||
stateCookie, err := g.Request.Cookie(ssoStateCookieKey)
|
||||
http.SetCookie(g.Writer, &http.Cookie{
|
||||
Name: ssoStateCookieKey,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
stateParam := g.Query("state")
|
||||
if err != nil || stateCookie.Value == "" || stateParam == "" ||
|
||||
subtle.ConstantTimeCompare([]byte(stateCookie.Value), []byte(stateParam)) != 1 {
|
||||
g.Redirect(http.StatusTemporaryRedirect, "/login?ssoAuthError=1")
|
||||
return
|
||||
}
|
||||
|
||||
code := g.Query("code")
|
||||
session, err := s.SSO.HandlEntraIDCallback(g, code)
|
||||
if err != nil {
|
||||
|
||||
@@ -583,7 +583,8 @@ func (c *User) Login(g *gin.Context) {
|
||||
return
|
||||
}
|
||||
// as the recovery code is valid, we can now disable MFA
|
||||
err = c.UserService.DisableTOTP(g, &userID)
|
||||
// no session exists yet during login so pass nil
|
||||
err = c.UserService.DisableTOTP(g, nil, &userID)
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
@@ -830,7 +831,7 @@ func (c *User) IsTOTPEnabled(g *gin.Context) {
|
||||
|
||||
// DisableTOTP disables TOTP
|
||||
func (c *User) DisableTOTP(g *gin.Context) {
|
||||
_, user, ok := c.handleSession(g)
|
||||
session, user, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -866,6 +867,7 @@ func (c *User) DisableTOTP(g *gin.Context) {
|
||||
// disable TOTP
|
||||
err = c.UserService.DisableTOTP(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
&userID,
|
||||
)
|
||||
// handle response
|
||||
|
||||
@@ -213,6 +213,14 @@ func (c *Common) responseWithZIP(
|
||||
}
|
||||
}
|
||||
|
||||
// responseWithPDF writes a PDF response
|
||||
func (c *Common) responseWithPDF(g *gin.Context, data []byte, name string) {
|
||||
setSecureContentDisposition(g, name)
|
||||
g.Header("Content-Type", "application/pdf")
|
||||
g.Header("Content-Length", fmt.Sprint(len(data)))
|
||||
_, _ = g.Writer.Write(data)
|
||||
}
|
||||
|
||||
// companyIDFromRequestQuery returns the companyID as a UUID from the query
|
||||
// or nil if not found
|
||||
func companyIDFromRequestQuery(g *gin.Context) *uuid.UUID {
|
||||
|
||||
Reference in New Issue
Block a user