mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-10-03 14:06:51 +02:00
323 lines
8.5 KiB
Go
323 lines
8.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/go-errors/errors"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/phishingclub/phishingclub/data"
|
|
"github.com/phishingclub/phishingclub/errs"
|
|
"github.com/phishingclub/phishingclub/model"
|
|
"github.com/phishingclub/phishingclub/repository"
|
|
"github.com/phishingclub/phishingclub/script"
|
|
"github.com/phishingclub/phishingclub/validate"
|
|
)
|
|
|
|
type Script struct {
|
|
Common
|
|
CampaignRepository *repository.Campaign
|
|
ScriptRepository *repository.Script
|
|
// TestRunner runs scripts in capture mode for the editor test panel. Nil when
|
|
// the feature is disabled.
|
|
TestRunner *script.Runner
|
|
}
|
|
|
|
// Test runs a script against a simulated event and returns what it did, without
|
|
// touching any campaign. Gated on the global admin permission.
|
|
func (a *Script) Test(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
script string,
|
|
event script.EventContext,
|
|
) (*script.TestResult, error) {
|
|
ae := NewAuditEvent("Script.Test", session)
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil {
|
|
a.LogAuthError(err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
a.AuditLogNotAuthorized(ae)
|
|
return nil, errs.ErrAuthorizationFailed
|
|
}
|
|
if a.TestRunner == nil {
|
|
return nil, errs.Wrap(errors.New("script is not enabled"))
|
|
}
|
|
// match the saved script cap (vo.String1MB) so a test run cannot submit an
|
|
// unbounded script
|
|
if len(script) > 1_000_000 {
|
|
return nil, validate.WrapErrorWithField(errors.New("script is too large"), "script")
|
|
}
|
|
result := a.TestRunner.RunTest(script, event)
|
|
a.AuditLogAuthorized(ae)
|
|
return result, nil
|
|
}
|
|
|
|
// RunCallable runs a saved script by name in callable mode (input in, returned
|
|
// object out), scoped to the company or a global script. Used by the remote
|
|
// browser runScript binding. No session: it is invoked server side from the
|
|
// victim flow, gated by the feature being enabled and the company scope.
|
|
func (a *Script) RunCallable(
|
|
ctx context.Context,
|
|
companyID *uuid.UUID,
|
|
name string,
|
|
input map[string]interface{},
|
|
) (map[string]interface{}, error) {
|
|
if a.TestRunner == nil {
|
|
return nil, errs.Wrap(errors.New("scripts are not enabled"))
|
|
}
|
|
sc, err := a.ScriptRepository.GetByNameScoped(ctx, name, companyID)
|
|
if err != nil {
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
source, err := sc.Script.Get()
|
|
if err != nil {
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
return a.TestRunner.RunCallable(ctx, source.String(), input)
|
|
}
|
|
|
|
// Create creates a new script
|
|
func (a *Script) Create(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
script *model.Script,
|
|
) (*uuid.UUID, error) {
|
|
ae := NewAuditEvent("Script.Create", session)
|
|
// check permissions
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil {
|
|
a.LogAuthError(err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
a.AuditLogNotAuthorized(ae)
|
|
return nil, errors.New("unauthorized")
|
|
}
|
|
// validate data
|
|
if err := script.Validate(); err != nil {
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
// check uniqueness
|
|
var companyID *uuid.UUID
|
|
if cid, err := script.CompanyID.Get(); err == nil {
|
|
companyID = &cid
|
|
}
|
|
name := script.Name.MustGet()
|
|
isOK, err := repository.CheckNameIsUnique(
|
|
ctx,
|
|
a.ScriptRepository.DB,
|
|
"scripts",
|
|
name.String(),
|
|
companyID,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
a.Logger.Errorw("failed to check script uniqueness", "error", err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
if !isOK {
|
|
a.Logger.Debugw("script name is already taken", "name", name.String())
|
|
return nil, validate.WrapErrorWithField(errors.New("is not unique"), "name")
|
|
}
|
|
// insert
|
|
id, err := a.ScriptRepository.Insert(ctx, script)
|
|
if err != nil {
|
|
a.Logger.Errorw("failed to insert script", "error", err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
ae.Details["id"] = id.String()
|
|
a.AuditLogAuthorized(ae)
|
|
|
|
return id, nil
|
|
}
|
|
|
|
// GetAll gets all scripts
|
|
func (a *Script) GetAll(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
companyID *uuid.UUID,
|
|
options *repository.ScriptOption,
|
|
) (*model.Result[model.Script], error) {
|
|
result := model.NewEmptyResult[model.Script]()
|
|
ae := NewAuditEvent("Script.GetAll", session)
|
|
if companyID != nil {
|
|
ae.Details["companyId"] = companyID.String()
|
|
}
|
|
// check permissions
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
a.LogAuthError(err)
|
|
return result, errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
a.AuditLogNotAuthorized(ae)
|
|
return result, errs.ErrAuthorizationFailed
|
|
}
|
|
// get
|
|
result, err = a.ScriptRepository.GetAll(ctx, companyID, options)
|
|
if err != nil {
|
|
a.Logger.Errorw("failed to get scripts", "error", err)
|
|
return result, errs.Wrap(err)
|
|
}
|
|
a.AuditLogAuthorized(ae)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// GetByID gets a script by id
|
|
func (a *Script) GetByID(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
id *uuid.UUID,
|
|
) (*model.Script, error) {
|
|
ae := NewAuditEvent("Script.GetByID", session)
|
|
// check permissions
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil {
|
|
a.LogAuthError(err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
a.AuditLogNotAuthorized(ae)
|
|
return nil, errs.ErrAuthorizationFailed
|
|
}
|
|
// get
|
|
out, err := a.ScriptRepository.GetByID(ctx, id)
|
|
if err != nil {
|
|
a.Logger.Errorw("failed to get script", "error", err)
|
|
return out, errs.Wrap(err)
|
|
}
|
|
// no audit on read
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// GetByCompanyID gets scripts by company id
|
|
func (a *Script) GetByCompanyID(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
companyID *uuid.UUID,
|
|
) ([]*model.Script, error) {
|
|
ae := NewAuditEvent("Script.GetByCompanyID", session)
|
|
if companyID != nil {
|
|
ae.Details["companyId"] = companyID.String()
|
|
}
|
|
// check permissions
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil {
|
|
a.LogAuthError(err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
a.AuditLogNotAuthorized(ae)
|
|
return nil, errs.ErrAuthorizationFailed
|
|
}
|
|
// get
|
|
models, err := a.ScriptRepository.GetAllByCompanyID(ctx, companyID, &repository.ScriptOption{})
|
|
if err != nil {
|
|
a.Logger.Errorw("failed to get scripts", "error", err)
|
|
return models, errs.Wrap(err)
|
|
}
|
|
// no audit on read
|
|
|
|
return models, nil
|
|
}
|
|
|
|
// Update updates a script
|
|
func (a *Script) Update(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
id *uuid.UUID,
|
|
script *model.Script,
|
|
) error {
|
|
ae := NewAuditEvent("Script.Update", session)
|
|
ae.Details["id"] = id.String()
|
|
// check permissions
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil {
|
|
a.LogAuthError(err)
|
|
return err
|
|
}
|
|
if !isAuthorized {
|
|
a.AuditLogNotAuthorized(ae)
|
|
return errors.New("unauthorized")
|
|
}
|
|
// confirm the script exists before updating
|
|
if _, err := a.ScriptRepository.GetByID(ctx, id); err != nil {
|
|
a.Logger.Errorw("failed to get script", "error", err)
|
|
return err
|
|
}
|
|
// the repository update reads the changed fields from the incoming script,
|
|
// so only the name uniqueness needs checking here
|
|
if v, err := script.Name.Get(); err == nil {
|
|
// check uniqueness
|
|
var companyID *uuid.UUID
|
|
if cid, err := script.CompanyID.Get(); err == nil {
|
|
companyID = &cid
|
|
}
|
|
|
|
isOK, err := repository.CheckNameIsUnique(
|
|
ctx,
|
|
a.ScriptRepository.DB,
|
|
"scripts",
|
|
v.String(),
|
|
companyID,
|
|
id,
|
|
)
|
|
if err != nil {
|
|
a.Logger.Errorw("failed to check script uniqueness", "error", err)
|
|
return err
|
|
}
|
|
if !isOK {
|
|
a.Logger.Debugw("script name is already taken", "name", v.String())
|
|
return validate.WrapErrorWithField(errors.New("is not unique"), "name")
|
|
}
|
|
}
|
|
// update
|
|
err = a.ScriptRepository.UpdateByID(ctx, id, script)
|
|
if err != nil {
|
|
a.Logger.Errorw("failed to update script", "error", err)
|
|
return err
|
|
}
|
|
a.AuditLogAuthorized(ae)
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteByID deletes a script
|
|
func (a *Script) DeleteByID(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
id *uuid.UUID,
|
|
) error {
|
|
ae := NewAuditEvent("Script.DeleteByID", session)
|
|
// check permissions
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil {
|
|
a.LogAuthError(err)
|
|
return err
|
|
}
|
|
if !isAuthorized {
|
|
a.AuditLogNotAuthorized(ae)
|
|
return errors.New("unauthorized")
|
|
}
|
|
// remove junction table rows for this script so no campaign retains
|
|
// a dangling reference
|
|
err = a.CampaignRepository.RemoveScriptFromJunctionByScriptID(ctx, id)
|
|
if err != nil {
|
|
a.Logger.Errorw("failed to remove script from campaign_scripts junction", "error", err)
|
|
return err
|
|
}
|
|
// delete
|
|
err = a.ScriptRepository.DeleteByID(ctx, id)
|
|
if err != nil {
|
|
a.Logger.Errorw("failed to delete script", "error", err)
|
|
return err
|
|
}
|
|
a.AuditLogAuthorized(ae)
|
|
|
|
return nil
|
|
}
|