Files
2026-09-30 22:50:37 +02:00

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
}