mirror of https://github.com/gophish/gophish
Merged into master
commit
74460f4d96
|
@ -0,0 +1,151 @@
|
|||
name: Build Gophish Release
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Binary
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
arch: ['386', amd64]
|
||||
# We sometimes use different verbiage for things (e.g. "darwin"
|
||||
# for the GOOS build flag and "osx" in the actual release ZIP).
|
||||
# We need to specify those here.
|
||||
include:
|
||||
- os: windows-latest
|
||||
goos: windows
|
||||
bin: 'gophish.exe'
|
||||
releaseos: windows
|
||||
- os: ubuntu-latest
|
||||
goos: linux
|
||||
bin: 'gophish'
|
||||
releaseos: linux
|
||||
- os: macos-latest
|
||||
goos: darwin
|
||||
bin: 'gophish'
|
||||
releaseos: osx
|
||||
# Don't build windows-32bit due to missing MinGW dependencies
|
||||
# Don't build osx-32bit due to eventual drop in Go support
|
||||
exclude:
|
||||
- os: windows-latest
|
||||
arch: '386'
|
||||
- os: macos-latest
|
||||
arch: '386'
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.14
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
run: sudo apt-get update && sudo apt-get install -y gcc-multilib
|
||||
- if: matrix.arch == '386'
|
||||
run: echo "::set-env name=RELEASE::gophish-${{ github.event.release.tag_name }}-${{ matrix.releaseos}}-32bit"
|
||||
- if: matrix.arch == 'amd64'
|
||||
run: echo "::set-env name=RELEASE::gophish-${{ github.event.release.tag_name}}-${{ matrix.releaseos}}-64bit"
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build ${{ matrix.goos }}/${{ matrix.arch }}
|
||||
run: go build -o ${{ matrix.bin }}
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
CGO_ENABLED: 1
|
||||
- name: Upload to artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ${{ env.RELEASE }}
|
||||
path: ${{ matrix.bin }}
|
||||
|
||||
package:
|
||||
name: Package Assets
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
path: bin
|
||||
- name: Package Releases
|
||||
run: |
|
||||
mkdir releases;
|
||||
for RELEASE_DIR in bin/*
|
||||
do
|
||||
echo "Creating release $RELEASE_DIR"
|
||||
for BINARY in $RELEASE_DIR/*
|
||||
do
|
||||
cp $BINARY .;
|
||||
zip -r releases/$(basename $RELEASE_DIR).zip \
|
||||
$(basename ${BINARY}) \
|
||||
static/js/dist \
|
||||
static/js/src/vendor/ckeditor \
|
||||
static/css/dist \
|
||||
static/images \
|
||||
static/font \
|
||||
static/db \
|
||||
db \
|
||||
templates \
|
||||
README.md \
|
||||
VERSION \
|
||||
LICENSE \
|
||||
config.json;
|
||||
rm $BINARY;
|
||||
done
|
||||
done
|
||||
- name: Upload to artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: releases
|
||||
path: releases/*.zip
|
||||
|
||||
upload:
|
||||
name: Upload to the Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: package
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: releases
|
||||
path: releases/
|
||||
# I would love to use @actions/upload-release-asset, but they don't
|
||||
# support wildcards in the asset path. Ref #9, #24, and #47
|
||||
- name: Upload Archives to Release
|
||||
env:
|
||||
UPLOAD_URL: ${{ github.event.release.upload_url }}
|
||||
API_HEADER: "Accept: application/vnd.github.v3+json"
|
||||
AUTH_HEADER: "Authorization: token ${{ secrets.GITHUB_TOKEN }}"
|
||||
run: |
|
||||
UPLOAD_URL=$(echo -n $UPLOAD_URL | sed s/\{.*//g)
|
||||
for FILE in releases/*
|
||||
do
|
||||
echo "Uploading ${FILE}";
|
||||
curl \
|
||||
-H "${API_HEADER}" \
|
||||
-H "${AUTH_HEADER}" \
|
||||
-H "Content-Type: $(file -b --mime-type ${FILE})" \
|
||||
--data-binary "@${FILE}" \
|
||||
"${UPLOAD_URL}?name=$(basename ${FILE})";
|
||||
done
|
||||
- name: Generate SHA256 Hashes
|
||||
env:
|
||||
API_HEADER: "Accept: application/vnd.github.v3+json"
|
||||
AUTH_HEADER: "Authorization: token ${{ secrets.GITHUB_TOKEN }}"
|
||||
RELEASE_URL: ${{ github.event.release.url }}
|
||||
run: |
|
||||
HASH_TABLE="| SHA256 Hash | Filename |"
|
||||
HASH_TABLE="${HASH_TABLE}\n|-----|-----|\n"
|
||||
for FILE in releases/*
|
||||
do
|
||||
FILENAME=$(basename ${FILE})
|
||||
HASH=$(sha256sum ${FILE} | cut -d ' ' -f 1)
|
||||
HASH_TABLE="${HASH_TABLE}|${HASH}|${FILENAME}|\n"
|
||||
done
|
||||
echo "${HASH_TABLE}"
|
||||
curl \
|
||||
-XPATCH \
|
||||
-H "${API_HEADER}" \
|
||||
-H "${AUTH_HEADER}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"body\": \"${HASH_TABLE}\"}" \
|
||||
"${RELEASE_URL}";
|
124
auth/auth.go
124
auth/auth.go
|
@ -1,69 +1,103 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"net/http"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/models"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// MinPasswordLength is the minimum number of characters required in a password
|
||||
const MinPasswordLength = 8
|
||||
|
||||
// APIKeyLength is the length of Gophish API keys
|
||||
const APIKeyLength = 32
|
||||
|
||||
// ErrInvalidPassword is thrown when a user provides an incorrect password.
|
||||
var ErrInvalidPassword = errors.New("Invalid Password")
|
||||
|
||||
// ErrPasswordMismatch is thrown when a user provides a blank password to the register
|
||||
// or change password functions
|
||||
var ErrPasswordMismatch = errors.New("Password cannot be blank")
|
||||
// ErrPasswordMismatch is thrown when a user provides a mismatching password
|
||||
// and confirmation password.
|
||||
var ErrPasswordMismatch = errors.New("Passwords do not match")
|
||||
|
||||
// ErrReusedPassword is thrown when a user attempts to change their password to
|
||||
// the existing password
|
||||
var ErrReusedPassword = errors.New("Cannot reuse existing password")
|
||||
|
||||
// ErrEmptyPassword is thrown when a user provides a blank password to the register
|
||||
// or change password functions
|
||||
var ErrEmptyPassword = errors.New("No password provided")
|
||||
|
||||
// Login attempts to login the user given a request.
|
||||
func Login(r *http.Request) (bool, models.User, error) {
|
||||
username, password := r.FormValue("username"), r.FormValue("password")
|
||||
u, err := models.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
return false, models.User{}, err
|
||||
}
|
||||
//If we've made it here, we should have a valid user stored in u
|
||||
//Let's check the password
|
||||
err = bcrypt.CompareHashAndPassword([]byte(u.Hash), []byte(password))
|
||||
if err != nil {
|
||||
return false, models.User{}, ErrInvalidPassword
|
||||
}
|
||||
return true, u, nil
|
||||
// ErrPasswordTooShort is thrown when a user provides a password that is less
|
||||
// than MinPasswordLength
|
||||
var ErrPasswordTooShort = fmt.Errorf("Password must be at least %d characters", MinPasswordLength)
|
||||
|
||||
// GenerateSecureKey returns the hex representation of key generated from n
|
||||
// random bytes
|
||||
func GenerateSecureKey(n int) string {
|
||||
k := make([]byte, n)
|
||||
io.ReadFull(rand.Reader, k)
|
||||
return fmt.Sprintf("%x", k)
|
||||
}
|
||||
|
||||
// ChangePassword verifies the current password provided in the request and,
|
||||
// if it's valid, changes the password for the authenticated user.
|
||||
func ChangePassword(r *http.Request) error {
|
||||
u := ctx.Get(r, "user").(models.User)
|
||||
currentPw := r.FormValue("current_password")
|
||||
newPassword := r.FormValue("new_password")
|
||||
confirmPassword := r.FormValue("confirm_new_password")
|
||||
// Check the current password
|
||||
err := bcrypt.CompareHashAndPassword([]byte(u.Hash), []byte(currentPw))
|
||||
// GeneratePasswordHash returns the bcrypt hash for the provided password using
|
||||
// the default bcrypt cost.
|
||||
func GeneratePasswordHash(password string) (string, error) {
|
||||
h, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return ErrInvalidPassword
|
||||
return "", err
|
||||
}
|
||||
// Check that the new password isn't blank
|
||||
if newPassword == "" {
|
||||
return string(h), nil
|
||||
}
|
||||
|
||||
// CheckPasswordPolicy ensures the provided password is valid according to our
|
||||
// password policy.
|
||||
//
|
||||
// The current password policy is simply a minimum of 8 characters, though this
|
||||
// may change in the future (see #1538).
|
||||
func CheckPasswordPolicy(password string) error {
|
||||
switch {
|
||||
// Admittedly, empty passwords are a subset of too short passwords, but it
|
||||
// helps to provide a more specific error message
|
||||
case password == "":
|
||||
return ErrEmptyPassword
|
||||
}
|
||||
// Check that new passwords match
|
||||
if newPassword != confirmPassword {
|
||||
return ErrPasswordMismatch
|
||||
}
|
||||
// Generate the new hash
|
||||
h, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Hash = string(h)
|
||||
if err = models.PutUser(&u); err != nil {
|
||||
return err
|
||||
case len(password) < MinPasswordLength:
|
||||
return ErrPasswordTooShort
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePassword validates that the provided password matches the provided
|
||||
// bcrypt hash.
|
||||
func ValidatePassword(password string, hash string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
}
|
||||
|
||||
// ValidatePasswordChange validates that the new password matches the
|
||||
// configured password policy, that the new password and confirmation
|
||||
// password match.
|
||||
//
|
||||
// Note that this assumes the current password has been confirmed by the
|
||||
// caller.
|
||||
//
|
||||
// If all of the provided data is valid, then the hash of the new password is
|
||||
// returned.
|
||||
func ValidatePasswordChange(currentHash, newPassword, confirmPassword string) (string, error) {
|
||||
// Ensure the new password passes our password policy
|
||||
if err := CheckPasswordPolicy(newPassword); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Check that new passwords match
|
||||
if newPassword != confirmPassword {
|
||||
return "", ErrPasswordMismatch
|
||||
}
|
||||
// Make sure that the new password isn't the same as the old one
|
||||
err := ValidatePassword(newPassword, currentHash)
|
||||
if err == nil {
|
||||
return "", ErrReusedPassword
|
||||
}
|
||||
// Generate the new hash
|
||||
return GeneratePasswordHash(newPassword)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPasswordPolicy(t *testing.T) {
|
||||
candidate := "short"
|
||||
got := CheckPasswordPolicy(candidate)
|
||||
if got != ErrPasswordTooShort {
|
||||
t.Fatalf("unexpected error received. expected %v got %v", ErrPasswordTooShort, got)
|
||||
}
|
||||
|
||||
candidate = "valid password"
|
||||
got = CheckPasswordPolicy(candidate)
|
||||
if got != nil {
|
||||
t.Fatalf("unexpected error received. expected %v got %v", nil, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePasswordChange(t *testing.T) {
|
||||
newPassword := "valid password"
|
||||
confirmPassword := "invalid"
|
||||
currentPassword := "current password"
|
||||
currentHash, err := GeneratePasswordHash(currentPassword)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error generating password hash: %v", err)
|
||||
}
|
||||
|
||||
_, got := ValidatePasswordChange(currentHash, newPassword, confirmPassword)
|
||||
if got != ErrPasswordMismatch {
|
||||
t.Fatalf("unexpected error received. expected %v got %v", ErrPasswordMismatch, got)
|
||||
}
|
||||
|
||||
newPassword = currentPassword
|
||||
confirmPassword = newPassword
|
||||
_, got = ValidatePasswordChange(currentHash, newPassword, confirmPassword)
|
||||
if got != ErrReusedPassword {
|
||||
t.Fatalf("unexpected error received. expected %v got %v", ErrReusedPassword, got)
|
||||
}
|
||||
}
|
|
@ -2,8 +2,9 @@ package config
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"io/ioutil"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
)
|
||||
|
||||
// AdminServer represents the Admin server configuration details
|
||||
|
@ -12,6 +13,7 @@ type AdminServer struct {
|
|||
UseTLS bool `json:"use_tls"`
|
||||
CertPath string `json:"cert_path"`
|
||||
KeyPath string `json:"key_path"`
|
||||
CSRFKey string `json:"csrf_key"`
|
||||
}
|
||||
|
||||
// PhishServer represents the Phish server configuration details
|
||||
|
|
|
@ -62,12 +62,13 @@ func TestLoadConfig(t *testing.T) {
|
|||
}
|
||||
expectedConfig.MigrationsPath = expectedConfig.MigrationsPath + expectedConfig.DBName
|
||||
expectedConfig.TestFlag = false
|
||||
expectedConfig.AdminConf.CSRFKey = ""
|
||||
if !reflect.DeepEqual(expectedConfig, conf) {
|
||||
t.Fatalf("invalid config received. expected %#v got %#v", expectedConfig, conf)
|
||||
}
|
||||
|
||||
// Load an invalid config
|
||||
conf, err = LoadConfig("bogusfile")
|
||||
_, err = LoadConfig("bogusfile")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error when loading invalid config, but got %v", err)
|
||||
}
|
||||
|
|
|
@ -23,6 +23,4 @@ func Set(r *http.Request, key, val interface{}) *http.Request {
|
|||
}
|
||||
|
||||
// Clear is a null operation, since this is handled automatically in Go > 1.7
|
||||
func Clear(r *http.Request) {
|
||||
return
|
||||
}
|
||||
func Clear(r *http.Request) {}
|
||||
|
|
|
@ -42,18 +42,6 @@ func setupTest(t *testing.T) *testContext {
|
|||
return ctx
|
||||
}
|
||||
|
||||
func tearDown(t *testing.T, ctx *testContext) {
|
||||
// Cleanup all users except the original admin
|
||||
// users, _ := models.GetUsers()
|
||||
// for _, user := range users {
|
||||
// if user.Id == 1 {
|
||||
// continue
|
||||
// }
|
||||
// err := models.DeleteUser(user.Id)
|
||||
// s.Nil(err)
|
||||
// }
|
||||
}
|
||||
|
||||
func createTestData(t *testing.T) {
|
||||
// Add a group
|
||||
group := models.Group{Name: "Test Group"}
|
||||
|
|
|
@ -87,6 +87,11 @@ func (as *Server) Group(w http.ResponseWriter, r *http.Request) {
|
|||
// Change this to get from URL and uid (don't bother with id in r.Body)
|
||||
g = models.Group{}
|
||||
err = json.NewDecoder(r.Body).Decode(&g)
|
||||
if err != nil {
|
||||
log.Errorf("error decoding group: %v", err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if g.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError)
|
||||
return
|
||||
|
|
|
@ -46,7 +46,6 @@ func (as *Server) ImportGroup(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
JSONResponse(w, ts, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// ImportEmail allows for the importing of email.
|
||||
|
@ -94,7 +93,6 @@ func (as *Server) ImportEmail(w http.ResponseWriter, r *http.Request) {
|
|||
HTML: string(e.HTML),
|
||||
}
|
||||
JSONResponse(w, er, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// ImportSite allows for the importing of HTML from a website
|
||||
|
@ -153,5 +151,4 @@ func (as *Server) ImportSite(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
cs := cloneResponse{HTML: h}
|
||||
JSONResponse(w, cs, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ package api
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gophish/gophish/auth"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
)
|
||||
|
||||
// Reset (/api/reset) resets the currently authenticated user's API key
|
||||
|
@ -13,7 +13,7 @@ func (as *Server) Reset(w http.ResponseWriter, r *http.Request) {
|
|||
switch {
|
||||
case r.Method == "POST":
|
||||
u := ctx.Get(r, "user").(models.User)
|
||||
u.ApiKey = util.GenerateSecureKey()
|
||||
u.ApiKey = auth.GenerateSecureKey(auth.APIKeyLength)
|
||||
err := models.PutUser(&u)
|
||||
if err != nil {
|
||||
http.Error(w, "Error setting API Key", http.StatusInternalServerError)
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
mid "github.com/gophish/gophish/middleware"
|
||||
"github.com/gophish/gophish/middleware/ratelimit"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/worker"
|
||||
"github.com/gorilla/mux"
|
||||
|
@ -19,14 +20,17 @@ type ServerOption func(*Server)
|
|||
type Server struct {
|
||||
handler http.Handler
|
||||
worker worker.Worker
|
||||
limiter *ratelimit.PostLimiter
|
||||
}
|
||||
|
||||
// NewServer returns a new instance of the API handler with the provided
|
||||
// options applied.
|
||||
func NewServer(options ...ServerOption) *Server {
|
||||
defaultWorker, _ := worker.New()
|
||||
defaultLimiter := ratelimit.NewPostLimiter()
|
||||
as := &Server{
|
||||
worker: defaultWorker,
|
||||
limiter: defaultLimiter,
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(as)
|
||||
|
@ -42,6 +46,12 @@ func WithWorker(w worker.Worker) ServerOption {
|
|||
}
|
||||
}
|
||||
|
||||
func WithLimiter(limiter *ratelimit.PostLimiter) ServerOption {
|
||||
return func(as *Server) {
|
||||
as.limiter = limiter
|
||||
}
|
||||
}
|
||||
|
||||
func (as *Server) registerRoutes() {
|
||||
root := mux.NewRouter()
|
||||
root = root.StrictSlash(true)
|
||||
|
|
|
@ -6,18 +6,14 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gophish/gophish/auth"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// ErrEmptyPassword is thrown when a user provides a blank password to the register
|
||||
// or change password functions
|
||||
var ErrEmptyPassword = errors.New("No password provided")
|
||||
|
||||
// ErrUsernameTaken is thrown when a user attempts to register a username that is taken.
|
||||
var ErrUsernameTaken = errors.New("Username already taken")
|
||||
|
||||
|
@ -36,6 +32,7 @@ type userRequest struct {
|
|||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Role string `json:"role"`
|
||||
PasswordChangeRequired bool `json:"password_change_required"`
|
||||
}
|
||||
|
||||
func (ur *userRequest) Validate(existingUser *models.User) error {
|
||||
|
@ -89,11 +86,12 @@ func (as *Server) Users(w http.ResponseWriter, r *http.Request) {
|
|||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if ur.Password == "" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: ErrEmptyPassword.Error()}, http.StatusBadRequest)
|
||||
err = auth.CheckPasswordPolicy(ur.Password)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
hash, err := util.NewHash(ur.Password)
|
||||
hash, err := auth.GeneratePasswordHash(ur.Password)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -106,7 +104,7 @@ func (as *Server) Users(w http.ResponseWriter, r *http.Request) {
|
|||
user := models.User{
|
||||
Username: ur.Username,
|
||||
Hash: hash,
|
||||
ApiKey: util.GenerateSecureKey(),
|
||||
ApiKey: auth.GenerateSecureKey(auth.APIKeyLength),
|
||||
Role: role,
|
||||
RoleID: role.ID,
|
||||
}
|
||||
|
@ -195,13 +193,20 @@ func (as *Server) User(w http.ResponseWriter, r *http.Request) {
|
|||
// We don't force the password to be provided, since it may be an admin
|
||||
// managing the user's account, and making a simple change like
|
||||
// updating the username or role. However, if it _is_ provided, we'll
|
||||
// update the stored hash.
|
||||
// update the stored hash after validating the new password meets our
|
||||
// password policy.
|
||||
//
|
||||
// Note that we don't force the current password to be provided. The
|
||||
// assumption here is that the API key is a proper bearer token proving
|
||||
// authenticated access to the account.
|
||||
existingUser.PasswordChangeRequired = ur.PasswordChangeRequired
|
||||
if ur.Password != "" {
|
||||
hash, err := util.NewHash(ur.Password)
|
||||
err = auth.CheckPasswordPolicy(ur.Password)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
hash, err := auth.GeneratePasswordHash(ur.Password)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
|
|
|
@ -66,7 +66,7 @@ func TestCreateUser(t *testing.T) {
|
|||
testCtx := setupTest(t)
|
||||
payload := &userRequest{
|
||||
Username: "foo",
|
||||
Password: "bar",
|
||||
Password: "validpassword",
|
||||
Role: models.RoleUser,
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
|
|
|
@ -118,5 +118,4 @@ func (as *Server) SendTestEmail(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Email Sent"}, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -62,15 +62,20 @@ func (as *Server) Webhook(w http.ResponseWriter, r *http.Request) {
|
|||
JSONResponse(w, models.Response{Success: true, Message: "Webhook deleted Successfully!"}, http.StatusOK)
|
||||
|
||||
case r.Method == "PUT":
|
||||
wh2 := models.Webhook{}
|
||||
err = json.NewDecoder(r.Body).Decode(&wh2)
|
||||
wh2.Id = id
|
||||
err = models.PutWebhook(&wh2)
|
||||
wh = models.Webhook{}
|
||||
err = json.NewDecoder(r.Body).Decode(&wh)
|
||||
if err != nil {
|
||||
log.Errorf("error decoding webhook: %v", err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
wh.Id = id
|
||||
err = models.PutWebhook(&wh)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, wh2, http.StatusOK)
|
||||
JSONResponse(w, wh, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gophish/gophish/auth"
|
||||
"github.com/gophish/gophish/config"
|
||||
"github.com/gophish/gophish/models"
|
||||
)
|
||||
|
@ -41,6 +42,10 @@ func setupTest(t *testing.T) *testContext {
|
|||
ctx.adminServer.Start()
|
||||
// Get the API key to use for these tests
|
||||
u, err := models.GetUser(1)
|
||||
// Reset the temporary password for the admin user to a value we control
|
||||
hash, err := auth.GeneratePasswordHash("gophish")
|
||||
u.Hash = hash
|
||||
models.PutUser(&u)
|
||||
if err != nil {
|
||||
t.Fatalf("error getting first user from database: %v", err)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package controllers
|
|||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
@ -86,9 +85,7 @@ func WithContactAddress(addr string) PhishingServerOption {
|
|||
func (ps *PhishingServer) Start() {
|
||||
if ps.config.UseTLS {
|
||||
// Only support TLS 1.2 and above - ref #1691, #1689
|
||||
ps.server.TLSConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
ps.server.TLSConfig = defaultTLSConfig
|
||||
err := util.CheckAndCreateSSL(ps.config.CertPath, ps.config.KeyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/gophish/gophish/controllers/api"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
mid "github.com/gophish/gophish/middleware"
|
||||
"github.com/gophish/gophish/middleware/ratelimit"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
"github.com/gophish/gophish/worker"
|
||||
|
@ -38,6 +39,28 @@ type AdminServer struct {
|
|||
server *http.Server
|
||||
worker worker.Worker
|
||||
config config.AdminServer
|
||||
limiter *ratelimit.PostLimiter
|
||||
}
|
||||
|
||||
var defaultTLSConfig = &tls.Config{
|
||||
PreferServerCipherSuites: true,
|
||||
CurvePreferences: []tls.CurveID{
|
||||
tls.X25519,
|
||||
tls.CurveP256,
|
||||
},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
|
||||
// Kept for backwards compatibility with some clients
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
},
|
||||
}
|
||||
|
||||
// WithWorker is an option that sets the background worker.
|
||||
|
@ -55,9 +78,11 @@ func NewAdminServer(config config.AdminServer, options ...AdminServerOption) *Ad
|
|||
ReadTimeout: 10 * time.Second,
|
||||
Addr: config.ListenURL,
|
||||
}
|
||||
defaultLimiter := ratelimit.NewPostLimiter()
|
||||
as := &AdminServer{
|
||||
worker: defaultWorker,
|
||||
server: defaultServer,
|
||||
limiter: defaultLimiter,
|
||||
config: config,
|
||||
}
|
||||
for _, opt := range options {
|
||||
|
@ -74,9 +99,7 @@ func (as *AdminServer) Start() {
|
|||
}
|
||||
if as.config.UseTLS {
|
||||
// Only support TLS 1.2 and above - ref #1691, #1689
|
||||
as.server.TLSConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
as.server.TLSConfig = defaultTLSConfig
|
||||
err := util.CheckAndCreateSSL(as.config.CertPath, as.config.KeyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -102,8 +125,9 @@ func (as *AdminServer) registerRoutes() {
|
|||
router := mux.NewRouter()
|
||||
// Base Front-end routes
|
||||
router.HandleFunc("/", mid.Use(as.Base, mid.RequireLogin))
|
||||
router.HandleFunc("/login", as.Login)
|
||||
router.HandleFunc("/login", mid.Use(as.Login, as.limiter.Limit))
|
||||
router.HandleFunc("/logout", mid.Use(as.Logout, mid.RequireLogin))
|
||||
router.HandleFunc("/reset_password", mid.Use(as.ResetPassword, mid.RequireLogin))
|
||||
router.HandleFunc("/campaigns", mid.Use(as.Campaigns, mid.RequireLogin))
|
||||
router.HandleFunc("/campaigns/{id:[0-9]+}", mid.Use(as.CampaignID, mid.RequireLogin))
|
||||
router.HandleFunc("/templates", mid.Use(as.Templates, mid.RequireLogin))
|
||||
|
@ -113,18 +137,25 @@ func (as *AdminServer) registerRoutes() {
|
|||
router.HandleFunc("/settings", mid.Use(as.Settings, mid.RequireLogin))
|
||||
router.HandleFunc("/users", mid.Use(as.UserManagement, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
|
||||
router.HandleFunc("/webhooks", mid.Use(as.Webhooks, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
|
||||
router.HandleFunc("/impersonate", mid.Use(as.Impersonate, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
|
||||
router.HandleFunc("/reported", mid.Use(as.Reported, mid.RequireLogin))
|
||||
router.HandleFunc("/reported/attachment/{id:[0-9]+}", mid.Use(as.ReportedEmailAttachment, mid.RequireLogin))
|
||||
|
||||
// Create the API routes
|
||||
api := api.NewServer(api.WithWorker(as.worker))
|
||||
api := api.NewServer(
|
||||
api.WithWorker(as.worker),
|
||||
api.WithLimiter(as.limiter),
|
||||
)
|
||||
router.PathPrefix("/api/").Handler(api)
|
||||
|
||||
// Setup static file serving
|
||||
router.PathPrefix("/").Handler(http.FileServer(unindexed.Dir("./static/")))
|
||||
|
||||
// Setup CSRF Protection
|
||||
csrfHandler := csrf.Protect([]byte(util.GenerateSecureKey()),
|
||||
csrfKey := []byte(as.config.CSRFKey)
|
||||
if len(csrfKey) == 0 {
|
||||
csrfKey = []byte(auth.GenerateSecureKey(auth.APIKeyLength))
|
||||
}
|
||||
csrfHandler := csrf.Protect(csrfKey,
|
||||
csrf.FieldName("csrf_token"),
|
||||
csrf.Secure(as.config.UseTLS))
|
||||
adminHandler := csrfHandler(router)
|
||||
|
@ -152,12 +183,14 @@ type templateParams struct {
|
|||
// the CSRF token.
|
||||
func newTemplateParams(r *http.Request) templateParams {
|
||||
user := ctx.Get(r, "user").(models.User)
|
||||
session := ctx.Get(r, "session").(*sessions.Session)
|
||||
modifySystem, _ := user.HasPermission(models.PermissionModifySystem)
|
||||
return templateParams{
|
||||
Token: csrf.Token(r),
|
||||
User: user,
|
||||
ModifySystem: modifySystem,
|
||||
Version: config.Version,
|
||||
Flashes: session.Flashes(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,22 +249,37 @@ func (as *AdminServer) Settings(w http.ResponseWriter, r *http.Request) {
|
|||
case r.Method == "GET":
|
||||
params := newTemplateParams(r)
|
||||
params.Title = "Settings"
|
||||
session := ctx.Get(r, "session").(*sessions.Session)
|
||||
session.Save(r, w)
|
||||
getTemplate(w, "settings").ExecuteTemplate(w, "base", params)
|
||||
case r.Method == "POST":
|
||||
err := auth.ChangePassword(r)
|
||||
u := ctx.Get(r, "user").(models.User)
|
||||
currentPw := r.FormValue("current_password")
|
||||
newPassword := r.FormValue("new_password")
|
||||
confirmPassword := r.FormValue("confirm_new_password")
|
||||
// Check the current password
|
||||
err := auth.ValidatePassword(currentPw, u.Hash)
|
||||
msg := models.Response{Success: true, Message: "Settings Updated Successfully"}
|
||||
if err == auth.ErrInvalidPassword {
|
||||
msg.Message = "Invalid Password"
|
||||
msg.Success = false
|
||||
api.JSONResponse(w, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
msg.Message = err.Error()
|
||||
msg.Success = false
|
||||
api.JSONResponse(w, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
newHash, err := auth.ValidatePasswordChange(u.Hash, newPassword, confirmPassword)
|
||||
if err != nil {
|
||||
msg.Message = err.Error()
|
||||
msg.Success = false
|
||||
api.JSONResponse(w, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
u.Hash = string(newHash)
|
||||
if err = models.PutUser(&u); err != nil {
|
||||
msg.Message = err.Error()
|
||||
msg.Success = false
|
||||
api.JSONResponse(w, msg, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
api.JSONResponse(w, msg, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
@ -244,6 +292,39 @@ func (as *AdminServer) UserManagement(w http.ResponseWriter, r *http.Request) {
|
|||
getTemplate(w, "users").ExecuteTemplate(w, "base", params)
|
||||
}
|
||||
|
||||
func (as *AdminServer) nextOrIndex(w http.ResponseWriter, r *http.Request) {
|
||||
next := "/"
|
||||
url, err := url.Parse(r.FormValue("next"))
|
||||
if err == nil {
|
||||
path := url.Path
|
||||
if path != "" {
|
||||
next = path
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, next, 302)
|
||||
}
|
||||
|
||||
func (as *AdminServer) handleInvalidLogin(w http.ResponseWriter, r *http.Request) {
|
||||
session := ctx.Get(r, "session").(*sessions.Session)
|
||||
Flash(w, r, "danger", "Invalid Username/Password")
|
||||
params := struct {
|
||||
User models.User
|
||||
Title string
|
||||
Flashes []interface{}
|
||||
Token string
|
||||
}{Title: "Login", Token: csrf.Token(r)}
|
||||
params.Flashes = session.Flashes()
|
||||
session.Save(r, w)
|
||||
templates := template.New("template")
|
||||
_, err := templates.ParseFiles("templates/login.html", "templates/flashes.html")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
// w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
template.Must(templates, err).ExecuteTemplate(w, "base", params)
|
||||
}
|
||||
|
||||
// Webhooks is an admin-only handler that handles webhooks
|
||||
func (as *AdminServer) Webhooks(w http.ResponseWriter, r *http.Request) {
|
||||
params := newTemplateParams(r)
|
||||
|
@ -281,6 +362,24 @@ func (as *AdminServer) ReportedEmailAttachment(w http.ResponseWriter, r *http.Re
|
|||
}
|
||||
}
|
||||
|
||||
// Impersonate allows an admin to login to a user account without needing the password
|
||||
func (as *AdminServer) Impersonate(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method == "POST" {
|
||||
username := r.FormValue("username")
|
||||
u, err := models.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
session := ctx.Get(r, "session").(*sessions.Session)
|
||||
session.Values["id"] = u.Id
|
||||
session.Save(r, w)
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
// Login handles the authentication flow for a user. If credentials are valid,
|
||||
// a session is created
|
||||
func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -302,37 +401,25 @@ func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
template.Must(templates, err).ExecuteTemplate(w, "base", params)
|
||||
case r.Method == "POST":
|
||||
//Attempt to login
|
||||
succ, u, err := auth.Login(r)
|
||||
// Find the user with the provided username
|
||||
username, password := r.FormValue("username"), r.FormValue("password")
|
||||
u, err := models.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
as.handleInvalidLogin(w, r)
|
||||
return
|
||||
}
|
||||
// Validate the user's password
|
||||
err = auth.ValidatePassword(password, u.Hash)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
as.handleInvalidLogin(w, r)
|
||||
return
|
||||
}
|
||||
// If we've logged in, save the session and redirect to the dashboard
|
||||
if succ {
|
||||
session.Values["id"] = u.Id
|
||||
session.Save(r, w)
|
||||
next := "/"
|
||||
url, err := url.Parse(r.FormValue("next"))
|
||||
if err == nil {
|
||||
path := url.Path
|
||||
if path != "" {
|
||||
next = path
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, next, 302)
|
||||
} else {
|
||||
Flash(w, r, "danger", "Invalid Username/Password")
|
||||
params.Flashes = session.Flashes()
|
||||
session.Save(r, w)
|
||||
templates := template.New("template")
|
||||
_, err := templates.ParseFiles("templates/login.html", "templates/flashes.html")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
template.Must(templates, err).ExecuteTemplate(w, "base", params)
|
||||
}
|
||||
as.nextOrIndex(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -342,9 +429,72 @@ func (as *AdminServer) Logout(w http.ResponseWriter, r *http.Request) {
|
|||
delete(session.Values, "id")
|
||||
Flash(w, r, "success", "You have successfully logged out")
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/login", 302)
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
}
|
||||
|
||||
// ResetPassword handles the password reset flow when a password change is
|
||||
// required either by the Gophish system or an administrator.
|
||||
//
|
||||
// This handler is meant to be used when a user is required to reset their
|
||||
// password, not just when they want to.
|
||||
//
|
||||
// This is an important distinction since in this handler we don't require
|
||||
// the user to re-enter their current password, as opposed to the flow
|
||||
// through the settings handler.
|
||||
//
|
||||
// To that end, if the user doesn't require a password change, we will
|
||||
// redirect them to the settings page.
|
||||
func (as *AdminServer) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
u := ctx.Get(r, "user").(models.User)
|
||||
session := ctx.Get(r, "session").(*sessions.Session)
|
||||
if !u.PasswordChangeRequired {
|
||||
Flash(w, r, "info", "Please reset your password through the settings page")
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/settings", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
params := newTemplateParams(r)
|
||||
params.Title = "Reset Password"
|
||||
switch {
|
||||
case r.Method == http.MethodGet:
|
||||
params.Flashes = session.Flashes()
|
||||
session.Save(r, w)
|
||||
getTemplate(w, "reset_password").ExecuteTemplate(w, "base", params)
|
||||
return
|
||||
case r.Method == http.MethodPost:
|
||||
newPassword := r.FormValue("password")
|
||||
confirmPassword := r.FormValue("confirm_password")
|
||||
newHash, err := auth.ValidatePasswordChange(u.Hash, newPassword, confirmPassword)
|
||||
if err != nil {
|
||||
Flash(w, r, "danger", err.Error())
|
||||
params.Flashes = session.Flashes()
|
||||
session.Save(r, w)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
getTemplate(w, "reset_password").ExecuteTemplate(w, "base", params)
|
||||
return
|
||||
}
|
||||
u.PasswordChangeRequired = false
|
||||
u.Hash = newHash
|
||||
if err = models.PutUser(&u); err != nil {
|
||||
Flash(w, r, "danger", err.Error())
|
||||
params.Flashes = session.Flashes()
|
||||
session.Save(r, w)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
getTemplate(w, "reset_password").ExecuteTemplate(w, "base", params)
|
||||
return
|
||||
}
|
||||
// TODO: We probably want to flash a message here that the password was
|
||||
// changed successfully. The problem is that when the user resets their
|
||||
// password on first use, they will see two flashes on the dashboard-
|
||||
// one for their password reset, and one for the "no campaigns created".
|
||||
//
|
||||
// The solution to this is to revamp the empty page to be more useful,
|
||||
// like a wizard or something.
|
||||
as.nextOrIndex(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make this execute the template, too
|
||||
func getTemplate(w http.ResponseWriter, tmpl string) *template.Template {
|
||||
templates := template.New("template")
|
||||
_, err := templates.ParseFiles("templates/base.html", "templates/nav.html", "templates/"+tmpl+".html", "templates/flashes.html")
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
ALTER TABLE `users` ADD COLUMN password_change_required BOOLEAN;
|
||||
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
ALTER TABLE users ADD COLUMN password_change_required BOOLEAN;
|
||||
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
|
6
go.mod
6
go.mod
|
@ -8,6 +8,8 @@ require (
|
|||
github.com/PuerkitoBio/goquery v1.5.0
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
|
||||
github.com/emersion/go-imap v1.0.4
|
||||
github.com/emersion/go-message v0.12.0
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/gophish/gomail v0.0.0-20180314010319-cf7e1a5479be
|
||||
github.com/gorilla/context v1.1.1
|
||||
|
@ -17,15 +19,15 @@ require (
|
|||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/jinzhu/gorm v1.9.12
|
||||
github.com/jordan-wright/email v0.0.0-20200121133829-a0b5c5b58bb6
|
||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e
|
||||
github.com/jordan-wright/unindexed v0.0.0-20181209214434-78fa79113c0f
|
||||
github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/mxk/go-imap v0.0.0-20150429134902-531c36c3f12d
|
||||
github.com/oschwald/maxminddb-golang v1.6.0
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/ziutek/mymysql v1.5.4 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
|
||||
|
|
21
go.sum
21
go.sum
|
@ -15,6 +15,15 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/emersion/go-imap v1.0.4 h1:uiCAIHM6Z5Jwkma1zdNDWWXxSCqb+/xHBkHflD7XBro=
|
||||
github.com/emersion/go-imap v1.0.4/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
|
||||
github.com/emersion/go-message v0.12.0 h1:mZnv35eZ6lB6EftTQBgYXspOH0FQdhpFhSUhA9i6/Zg=
|
||||
github.com/emersion/go-message v0.12.0/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
|
@ -43,8 +52,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
|||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
|
||||
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jordan-wright/email v0.0.0-20200121133829-a0b5c5b58bb6 h1:gI29NnCaNU8N7rZT2svjtas5SrbL0XsutOPtInVvGIA=
|
||||
github.com/jordan-wright/email v0.0.0-20200121133829-a0b5c5b58bb6/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e h1:OGunVjqY7y4U4laftpEHv+mvZBlr7UGimJXKEGQtg48=
|
||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY=
|
||||
github.com/jordan-wright/unindexed v0.0.0-20181209214434-78fa79113c0f h1:bYVTBvVHcAYDkH8hyVMRUW7J2mYQNNSmQPXGadYd1nY=
|
||||
github.com/jordan-wright/unindexed v0.0.0-20181209214434-78fa79113c0f/go.mod h1:eRt05O5haIXGKGodWjpQ2xdgBHTE7hg/pzsukNi9IRA=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
|
@ -53,11 +62,10 @@ github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28 h1:mkl3tvPHIuP
|
|||
github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c=
|
||||
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mxk/go-imap v0.0.0-20150429134902-531c36c3f12d h1:+DgqA2tuWi/8VU+gVgBAa7+WZrnFbPKhQWbKBB54cVs=
|
||||
github.com/mxk/go-imap v0.0.0-20150429134902-531c36c3f12d/go.mod h1:xacC5qXZnL/ooiitVoe3BtI1OotFTqi5zICBs9J5Fyk=
|
||||
github.com/oschwald/maxminddb-golang v1.6.0 h1:KAJSjdHQ8Kv45nFIbtoLGrGWqHFajOIm7skTyz/+Dls=
|
||||
github.com/oschwald/maxminddb-golang v1.6.0/go.mod h1:DUJFucBg2cvqx42YmDa/+xHvb0elJtOm3o4aFQ/nb/w=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
|
@ -90,6 +98,11 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 h1:Dho5nD6R3PcW2SH1or8vS0dszDaXRxIw55lBX7XiE5g=
|
||||
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
|
|
22
gophish.go
22
gophish.go
|
@ -26,6 +26,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|||
THE SOFTWARE.
|
||||
*/
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
@ -40,9 +41,17 @@ import (
|
|||
"github.com/gophish/gophish/models"
|
||||
)
|
||||
|
||||
const (
|
||||
modeAll string = "all"
|
||||
modeAdmin string = "admin"
|
||||
modePhish string = "phish"
|
||||
)
|
||||
|
||||
var (
|
||||
configPath = kingpin.Flag("config", "Location of config.json.").Default("./config.json").String()
|
||||
disableMailer = kingpin.Flag("disable-mailer", "Disable the mailer (for use with multi-system deployments)").Bool()
|
||||
mode = kingpin.Flag("mode", fmt.Sprintf("Run the binary in one of the modes (%s, %s or %s)", modeAll, modeAdmin, modePhish)).
|
||||
Default("all").Enum(modeAll, modeAdmin, modePhish)
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -102,18 +111,25 @@ func main() {
|
|||
phishServer := controllers.NewPhishingServer(phishConfig)
|
||||
|
||||
imapMonitor := imap.NewMonitor()
|
||||
|
||||
if *mode == "admin" || *mode == "all" {
|
||||
go adminServer.Start()
|
||||
go phishServer.Start()
|
||||
go imapMonitor.Start()
|
||||
}
|
||||
if *mode == "phish" || *mode == "all" {
|
||||
go phishServer.Start()
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
<-c
|
||||
log.Info("CTRL+C Received... Gracefully shutting down servers")
|
||||
if *mode == modeAdmin || *mode == modeAll {
|
||||
adminServer.Shutdown()
|
||||
phishServer.Shutdown()
|
||||
imapMonitor.Shutdown()
|
||||
}
|
||||
if *mode == modePhish || *mode == modeAll {
|
||||
phishServer.Shutdown()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
45
imap/imap.go
45
imap/imap.go
|
@ -4,7 +4,6 @@ import (
|
|||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
@ -12,7 +11,6 @@ import (
|
|||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/emersion/go-message/charset"
|
||||
"github.com/emersion/go-message/mail"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
|
||||
|
@ -48,7 +46,6 @@ type Mailbox struct {
|
|||
|
||||
// Validate validates supplied IMAP model by connecting to the server
|
||||
func Validate(s *models.IMAP) error {
|
||||
|
||||
err := s.Validate()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
@ -117,7 +114,6 @@ func (mbox *Mailbox) DeleteEmails(seqs []uint32) error {
|
|||
|
||||
// GetUnread will find all unread emails in the folder and return them as a list.
|
||||
func (mbox *Mailbox) GetUnread(markAsRead, delete bool) ([]Email, error) {
|
||||
|
||||
imap.CharsetReader = charset.Reader
|
||||
var emails []Email
|
||||
|
||||
|
@ -130,13 +126,16 @@ func (mbox *Mailbox) GetUnread(markAsRead, delete bool) ([]Email, error) {
|
|||
|
||||
// Search for unread emails
|
||||
criteria := imap.NewSearchCriteria()
|
||||
criteria.WithoutFlags = []string{"\\Seen"}
|
||||
criteria.WithoutFlags = []string{imap.SeenFlag}
|
||||
seqs, err := imapClient.Search(criteria)
|
||||
if err != nil {
|
||||
return emails, err
|
||||
}
|
||||
|
||||
if len(seqs) > 0 {
|
||||
if len(seqs) == 0 {
|
||||
return emails, nil
|
||||
}
|
||||
|
||||
seqset := new(imap.SeqSet)
|
||||
seqset.AddNum(seqs...)
|
||||
section := &imap.BodySectionName{}
|
||||
|
@ -172,38 +171,9 @@ func (mbox *Mailbox) GetUnread(markAsRead, delete bool) ([]Email, error) {
|
|||
return emails, err
|
||||
}
|
||||
|
||||
// Reload the reader
|
||||
rawBodyStream = bytes.NewReader(buf)
|
||||
mr, err := mail.CreateReader(rawBodyStream)
|
||||
if err != nil {
|
||||
return emails, err
|
||||
}
|
||||
|
||||
// Step over each part of the email, parsing attachments and attaching them to Jordan's email
|
||||
for {
|
||||
p, err := mr.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return emails, err
|
||||
}
|
||||
h := p.Header
|
||||
|
||||
s, ok := h.(*mail.AttachmentHeader)
|
||||
if ok {
|
||||
filename, _ := s.Filename()
|
||||
typ, _, _ := s.ContentType()
|
||||
_, err := em.Attach(p.Body, filename, typ)
|
||||
if err != nil {
|
||||
return emails, err //Unable to attach file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emtmp := Email{Email: em, SeqNum: msg.SeqNum} // Not sure why msg.Uid is always 0, so swapped to sequence numbers
|
||||
emails = append(emails, emtmp)
|
||||
|
||||
} // On to the next email
|
||||
}
|
||||
return emails, nil
|
||||
}
|
||||
|
@ -214,15 +184,12 @@ func (mbox *Mailbox) newClient() (*client.Client, error) {
|
|||
var err error
|
||||
if mbox.TLS {
|
||||
imapClient, err = client.DialTLS(mbox.Host, new(tls.Config))
|
||||
if err != nil {
|
||||
return imapClient, err
|
||||
}
|
||||
} else {
|
||||
imapClient, err = client.Dial(mbox.Host)
|
||||
}
|
||||
if err != nil {
|
||||
return imapClient, err
|
||||
}
|
||||
}
|
||||
|
||||
err = imapClient.Login(mbox.User, mbox.Pwd)
|
||||
if err != nil {
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"context"
|
||||
"encoding/base64"
|
||||
"net/mail"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -34,7 +35,6 @@ type Monitor struct {
|
|||
// As each account can have its own polling frequency set we need to run one Go routine for
|
||||
// each, as well as keeping an eye on newly created user accounts.
|
||||
func (im *Monitor) start(ctx context.Context) {
|
||||
|
||||
usermap := make(map[int64]int) // Keep track of running go routines, one per user. We assume incrementing non-repeating UIDs (for the case where users are deleted and re-added).
|
||||
|
||||
for {
|
||||
|
@ -62,7 +62,6 @@ func (im *Monitor) start(ctx context.Context) {
|
|||
// monitor will continuously login to the IMAP settings associated to the supplied user id (if the user account has IMAP settings, and they're enabled.)
|
||||
// It also verifies the user account exists, and returns if not (for the case of a user being deleted).
|
||||
func monitor(uid int64, ctx context.Context) {
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
@ -96,7 +95,6 @@ func monitor(uid int64, ctx context.Context) {
|
|||
|
||||
// NewMonitor returns a new instance of imap.Monitor
|
||||
func NewMonitor() *Monitor {
|
||||
|
||||
im := &Monitor{}
|
||||
return im
|
||||
}
|
||||
|
@ -120,7 +118,6 @@ func (im *Monitor) Shutdown() error {
|
|||
// checkForNewEmails logs into an IMAP account and checks unread emails
|
||||
// for the rid campaign identifier.
|
||||
func checkForNewEmails(im models.IMAP) {
|
||||
|
||||
im.Host = im.Host + ":" + strconv.Itoa(int(im.Port)) // Append port
|
||||
mailServer := Mailbox{
|
||||
Host: im.Host,
|
||||
|
@ -140,7 +137,7 @@ func checkForNewEmails(im models.IMAP) {
|
|||
if len(msgs) > 0 {
|
||||
log.Debugf("%d new emails for %s", len(msgs), im.Username)
|
||||
var reportingFailed []uint32 // SeqNums of emails that were unable to be reported to phishing server, mark as unread
|
||||
var campaignEmails []uint32 // SeqNums of campaign emails. If DeleteReportedCampaignEmail is true, we will delete these
|
||||
var deleteEmails []uint32 // SeqNums of campaign emails. If DeleteReportedCampaignEmail is true, we will delete these
|
||||
for _, m := range msgs {
|
||||
// Check if sender is from company's domain, if enabled. TODO: Make this an IMAP filter
|
||||
if im.RestrictDomain != "" { // e.g domainResitct = widgets.com
|
||||
|
@ -152,12 +149,14 @@ func checkForNewEmails(im models.IMAP) {
|
|||
}
|
||||
}
|
||||
|
||||
rids, err := checkRIDs(m.Email) // Search email Text, HTML, and each attachment for rid parameters
|
||||
rids, err := matchEmail(m.Email) // Search email Text, HTML, and each attachment for rid parameters
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Error searching email for rids from user '%s': %s", m.Email.From, err.Error())
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if len(rids) < 1 {
|
||||
// In the future this should be an alert in Gophish
|
||||
log.Infof("User '%s' reported email with subject '%s'. This is not a GoPhish campaign; you should investigate it.", m.Email.From, m.Email.Subject)
|
||||
|
||||
// Save reported email to the database
|
||||
|
@ -185,6 +184,7 @@ func checkForNewEmails(im models.IMAP) {
|
|||
Status: "Unknown"}
|
||||
|
||||
models.SaveReportedEmail(em)
|
||||
|
||||
}
|
||||
for rid := range rids {
|
||||
log.Infof("User '%s' reported email with rid %s", m.Email.From, rid)
|
||||
|
@ -192,20 +192,19 @@ func checkForNewEmails(im models.IMAP) {
|
|||
if err != nil {
|
||||
log.Error("Error reporting GoPhish email with rid ", rid, ": ", err.Error())
|
||||
reportingFailed = append(reportingFailed, m.SeqNum)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
err = result.HandleEmailReport(models.EventDetails{})
|
||||
if err != nil {
|
||||
log.Error("Error updating GoPhish email with rid ", rid, ": ", err.Error())
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if im.DeleteReportedCampaignEmail == true {
|
||||
campaignEmails = append(campaignEmails, m.SeqNum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleteEmails = append(deleteEmails, m.SeqNum)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// Check if any emails were unable to be reported, so we can mark them as unread
|
||||
if len(reportingFailed) > 0 {
|
||||
log.Debugf("Marking %d emails as unread as failed to report", len(reportingFailed))
|
||||
|
@ -215,55 +214,48 @@ func checkForNewEmails(im models.IMAP) {
|
|||
}
|
||||
}
|
||||
// If the DeleteReportedCampaignEmail flag is set, delete reported Gophish campaign emails
|
||||
if im.DeleteReportedCampaignEmail == true && len(campaignEmails) > 0 {
|
||||
log.Debugf("Deleting %d campaign emails", len(campaignEmails))
|
||||
err := mailServer.DeleteEmails(campaignEmails) // Delete GoPhish campaign emails.
|
||||
if len(deleteEmails) > 0 {
|
||||
log.Debugf("Deleting %d campaign emails", len(deleteEmails))
|
||||
err := mailServer.DeleteEmails(deleteEmails) // Delete GoPhish campaign emails.
|
||||
if err != nil {
|
||||
log.Error("Failed to delete emails: ", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Debug("No new emails for ", im.Username)
|
||||
}
|
||||
}
|
||||
|
||||
// returns a slice of gophish rid paramters found in the email HTML, Text, and attachments
|
||||
func checkRIDs(em *email.Email) (map[string]int, error) {
|
||||
|
||||
rids := make(map[string]int)
|
||||
|
||||
func checkRIDs(em *email.Email, rids map[string]bool) {
|
||||
// Check Text and HTML
|
||||
emailContent := string(em.Text) + string(em.HTML)
|
||||
for _, r := range goPhishRegex.FindAllStringSubmatch(emailContent, -1) {
|
||||
newrid := r[len(r)-1]
|
||||
if _, ok := rids[newrid]; ok {
|
||||
rids[newrid]++
|
||||
} else {
|
||||
rids[newrid] = 1
|
||||
if !rids[newrid] {
|
||||
rids[newrid] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns a slice of gophish rid paramters found in the email HTML, Text, and attachments
|
||||
func matchEmail(em *email.Email) (map[string]bool, error) {
|
||||
rids := make(map[string]bool)
|
||||
checkRIDs(em, rids)
|
||||
|
||||
// Next check each attachment
|
||||
for _, a := range em.Attachments {
|
||||
if a.Header.Get("Content-Type") == "message/rfc822" || (len(a.Filename) > 3 && a.Filename[len(a.Filename)-4:] == ".eml") {
|
||||
ext := filepath.Ext(a.Filename)
|
||||
if a.Header.Get("Content-Type") == "message/rfc822" || ext == ".eml" {
|
||||
|
||||
// Let's decode the email
|
||||
rawBodyStream := bytes.NewReader(a.Content)
|
||||
attachementEmail, err := email.NewEmailFromReader(rawBodyStream)
|
||||
attachmentEmail, err := email.NewEmailFromReader(rawBodyStream)
|
||||
if err != nil {
|
||||
return rids, err
|
||||
}
|
||||
|
||||
emailContent := string(attachementEmail.Text) + string(attachementEmail.HTML)
|
||||
for _, r := range goPhishRegex.FindAllStringSubmatch(emailContent, -1) {
|
||||
newrid := r[len(r)-1]
|
||||
if _, ok := rids[newrid]; ok {
|
||||
rids[newrid]++
|
||||
} else {
|
||||
rids[newrid] = 1
|
||||
}
|
||||
}
|
||||
checkRIDs(attachmentEmail, rids)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,11 +13,6 @@ import (
|
|||
// being unreachable
|
||||
var errHostUnreachable = errors.New("host unreachable")
|
||||
|
||||
// errDialerUnavailable is a mock error to represent a dialer
|
||||
// being unavailable (perhaps an error getting the dialer config
|
||||
// or a database error)
|
||||
var errDialerUnavailable = errors.New("dialer unavailable")
|
||||
|
||||
// mockDialer keeps track of calls to Dial
|
||||
type mockDialer struct {
|
||||
dialCount int
|
||||
|
@ -137,10 +132,6 @@ func (mm *mockMessage) defaultDialer() (Dialer, error) {
|
|||
return newMockDialer(), nil
|
||||
}
|
||||
|
||||
func (mm *mockMessage) errorDialer() (Dialer, error) {
|
||||
return nil, errDialerUnavailable
|
||||
}
|
||||
|
||||
func (mm *mockMessage) GetDialer() (Dialer, error) {
|
||||
return mm.getdialer()
|
||||
}
|
||||
|
|
|
@ -114,13 +114,21 @@ func RequireAPIKey(handler http.Handler) http.Handler {
|
|||
func RequireLogin(handler http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if u := ctx.Get(r, "user"); u != nil {
|
||||
// If a password change is required for the user, then redirect them
|
||||
// to the login page
|
||||
currentUser := u.(models.User)
|
||||
if currentUser.PasswordChangeRequired && r.URL.Path != "/reset_password" {
|
||||
q := r.URL.Query()
|
||||
q.Set("next", r.URL.Path)
|
||||
http.Redirect(w, r, fmt.Sprintf("/reset_password?%s", q.Encode()), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
q := r.URL.Query()
|
||||
q.Set("next", r.URL.Path)
|
||||
http.Redirect(w, r, fmt.Sprintf("/login?%s", q.Encode()), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -162,3 +162,22 @@ func TestBearerToken(t *testing.T) {
|
|||
t.Fatalf("incorrect status code received. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordResetRequired(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req = ctx.Set(req, "user", models.User{
|
||||
PasswordChangeRequired: true,
|
||||
})
|
||||
response := httptest.NewRecorder()
|
||||
RequireLogin(successHandler).ServeHTTP(response, req)
|
||||
gotStatus := response.Code
|
||||
expectedStatus := http.StatusTemporaryRedirect
|
||||
if gotStatus != expectedStatus {
|
||||
t.Fatalf("incorrect status code received. expected %d got %d", expectedStatus, gotStatus)
|
||||
}
|
||||
expectedLocation := "/reset_password?next=%2F"
|
||||
gotLocation := response.Header().Get("Location")
|
||||
if gotLocation != expectedLocation {
|
||||
t.Fatalf("incorrect location header received. expected %s got %s", expectedLocation, gotLocation)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
// Package ratelimit provides a simple token-bucket rate limiting middleware
|
||||
// which only allows n POST requests every minute. This is meant to be used on
|
||||
// login handlers or other sensitive transactions which should be throttled to
|
||||
// prevent abuse.
|
||||
//
|
||||
// Tracked clients are stored in a locked map, with a goroutine that runs at a
|
||||
// configurable interval to clean up stale entries.
|
||||
//
|
||||
// Note that there is no enforcement for GET requests. This is an effort to be
|
||||
// opinionated in order to hit the most common use-cases. For more advanced
|
||||
// use-cases, you may consider the `github.com/didip/tollbooth` package.
|
||||
//
|
||||
// The enforcement mechanism is based on the blog post here:
|
||||
// https://www.alexedwards.net/blog/how-to-rate-limit-http-requests
|
||||
package ratelimit
|
|
@ -0,0 +1,145 @@
|
|||
package ratelimit
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// DefaultRequestsPerMinute is the number of requests to allow per minute.
|
||||
// Any requests over this interval will return a HTTP 429 error.
|
||||
const DefaultRequestsPerMinute = 5
|
||||
|
||||
// DefaultCleanupInterval determines how frequently the cleanup routine
|
||||
// executes.
|
||||
const DefaultCleanupInterval = 1 * time.Minute
|
||||
|
||||
// DefaultExpiry is the amount of time to track a bucket for a particular
|
||||
// visitor.
|
||||
const DefaultExpiry = 10 * time.Minute
|
||||
|
||||
type bucket struct {
|
||||
limiter *rate.Limiter
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
// PostLimiter is a simple rate limiting middleware which only allows n POST
|
||||
// requests per minute.
|
||||
type PostLimiter struct {
|
||||
visitors map[string]*bucket
|
||||
requestLimit int
|
||||
cleanupInterval time.Duration
|
||||
expiry time.Duration
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// PostLimiterOption is a functional option that allows callers to configure
|
||||
// the rate limiter.
|
||||
type PostLimiterOption func(*PostLimiter)
|
||||
|
||||
// WithRequestsPerMinute sets the number of requests to allow per minute.
|
||||
func WithRequestsPerMinute(requestLimit int) PostLimiterOption {
|
||||
return func(p *PostLimiter) {
|
||||
p.requestLimit = requestLimit
|
||||
}
|
||||
}
|
||||
|
||||
// WithCleanupInterval sets the interval between cleaning up stale entries in
|
||||
// the rate limit client list
|
||||
func WithCleanupInterval(interval time.Duration) PostLimiterOption {
|
||||
return func(p *PostLimiter) {
|
||||
p.cleanupInterval = interval
|
||||
}
|
||||
}
|
||||
|
||||
// WithExpiry sets the amount of time to store client entries before they are
|
||||
// considered stale.
|
||||
func WithExpiry(expiry time.Duration) PostLimiterOption {
|
||||
return func(p *PostLimiter) {
|
||||
p.expiry = expiry
|
||||
}
|
||||
}
|
||||
|
||||
// NewPostLimiter returns a new instance of a PostLimiter
|
||||
func NewPostLimiter(opts ...PostLimiterOption) *PostLimiter {
|
||||
limiter := &PostLimiter{
|
||||
visitors: make(map[string]*bucket),
|
||||
requestLimit: DefaultRequestsPerMinute,
|
||||
cleanupInterval: DefaultCleanupInterval,
|
||||
expiry: DefaultExpiry,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(limiter)
|
||||
}
|
||||
go limiter.pollCleanup()
|
||||
return limiter
|
||||
}
|
||||
|
||||
func (limiter *PostLimiter) pollCleanup() {
|
||||
ticker := time.NewTicker(time.Duration(limiter.cleanupInterval) * time.Second)
|
||||
for range ticker.C {
|
||||
limiter.Cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup removes any buckets that were last seen past the configured expiry.
|
||||
func (limiter *PostLimiter) Cleanup() {
|
||||
limiter.Lock()
|
||||
defer limiter.Unlock()
|
||||
for ip, bucket := range limiter.visitors {
|
||||
if time.Now().Sub(bucket.lastSeen) >= limiter.expiry {
|
||||
delete(limiter.visitors, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (limiter *PostLimiter) addBucket(ip string) *bucket {
|
||||
limiter.Lock()
|
||||
defer limiter.Unlock()
|
||||
limit := rate.NewLimiter(rate.Every(time.Minute/time.Duration(limiter.requestLimit)), limiter.requestLimit)
|
||||
b := &bucket{
|
||||
limiter: limit,
|
||||
}
|
||||
limiter.visitors[ip] = b
|
||||
return b
|
||||
}
|
||||
|
||||
func (limiter *PostLimiter) allow(ip string) bool {
|
||||
// Check if we have a limiter already active for this clientIP
|
||||
limiter.RLock()
|
||||
bucket, exists := limiter.visitors[ip]
|
||||
limiter.RUnlock()
|
||||
if !exists {
|
||||
bucket = limiter.addBucket(ip)
|
||||
}
|
||||
// Update the lastSeen for this bucket to assist with cleanup
|
||||
limiter.Lock()
|
||||
defer limiter.Unlock()
|
||||
bucket.lastSeen = time.Now()
|
||||
return bucket.limiter.Allow()
|
||||
}
|
||||
|
||||
// Limit enforces the configured rate limit for POST requests.
|
||||
//
|
||||
// TODO: Change the return value to an http.Handler when we clean up the
|
||||
// way Gophish routing is done.
|
||||
func (limiter *PostLimiter) Limit(next http.Handler) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to determine client IP address: %v", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodPost && !limiter.allow(clientIP) {
|
||||
log.Error("")
|
||||
http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package ratelimit
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var successHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
func reachLimit(t *testing.T, handler http.Handler, limit int) {
|
||||
// Make `expected` requests and ensure that each return a successful
|
||||
// response.
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
r.RemoteAddr = "127.0.0.1:"
|
||||
for i := 0; i < limit; i++ {
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("no 200 on req %d got %d", i, w.Code)
|
||||
}
|
||||
}
|
||||
// Then, makes another request to ensure it returns the 429
|
||||
// status.
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusTooManyRequests {
|
||||
t.Fatalf("no 429")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimitEnforcement(t *testing.T) {
|
||||
expectedLimit := 3
|
||||
limiter := NewPostLimiter(WithRequestsPerMinute(expectedLimit))
|
||||
handler := limiter.Limit(successHandler)
|
||||
reachLimit(t, handler, expectedLimit)
|
||||
}
|
||||
|
||||
func TestRateLimitCleanup(t *testing.T) {
|
||||
expectedLimit := 3
|
||||
limiter := NewPostLimiter(WithRequestsPerMinute(expectedLimit))
|
||||
handler := limiter.Limit(successHandler)
|
||||
reachLimit(t, handler, expectedLimit)
|
||||
|
||||
// Set the timeout to be
|
||||
bucket, exists := limiter.visitors["127.0.0.1"]
|
||||
if !exists {
|
||||
t.Fatalf("doesn't exist for some reason")
|
||||
}
|
||||
bucket.lastSeen = bucket.lastSeen.Add(-limiter.expiry)
|
||||
limiter.Cleanup()
|
||||
_, exists = limiter.visitors["127.0.0.1"]
|
||||
if exists {
|
||||
t.Fatalf("exists for some reason")
|
||||
}
|
||||
reachLimit(t, handler, expectedLimit)
|
||||
}
|
|
@ -27,7 +27,7 @@ type Campaign struct {
|
|||
Status string `json:"status"`
|
||||
Results []Result `json:"results,omitempty"`
|
||||
Groups []Group `json:"groups,omitempty"`
|
||||
Events []Event `json:"timeline,omitemtpy"`
|
||||
Events []Event `json:"timeline,omitempty"`
|
||||
SMTPId int64 `json:"-"`
|
||||
SMTP SMTP `json:"smtp"`
|
||||
URL string `json:"url"`
|
||||
|
|
|
@ -103,10 +103,6 @@ func (s *EmailRequest) Generate(msg *gomail.Message) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fn := f.Name
|
||||
if fn == "" {
|
||||
fn = f.Address
|
||||
}
|
||||
msg.SetAddressHeader("From", f.Address, f.Name)
|
||||
|
||||
ptx, err := NewPhishingTemplateContext(s, s.BaseRecipient, s.RId)
|
||||
|
|
|
@ -226,7 +226,6 @@ func PutGroup(g *Group) error {
|
|||
return err
|
||||
}
|
||||
// Fetch group's existing targets from database.
|
||||
ts := []Target{}
|
||||
ts, err := GetTargets(g.Id)
|
||||
if err != nil {
|
||||
log.WithFields(logrus.Fields{
|
||||
|
|
|
@ -53,7 +53,7 @@ var ErrIMAPPasswordNotSpecified = errors.New("No Password specified")
|
|||
|
||||
// ErrInvalidIMAPFreq is thrown when the frequency for polling the
|
||||
// IMAP server is invalid
|
||||
var ErrInvalidIMAPFreq = errors.New("Invalid polling frequency.")
|
||||
var ErrInvalidIMAPFreq = errors.New("Invalid polling frequency")
|
||||
|
||||
// TableName specifies the database tablename for Gorm to use
|
||||
func (im IMAP) TableName() string {
|
||||
|
|
|
@ -125,7 +125,7 @@ func (m *MailLog) Success() error {
|
|||
return err
|
||||
}
|
||||
err = db.Delete(m).Error
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDialer returns a dialer based on the maillog campaign's SMTP configuration
|
||||
|
|
|
@ -7,13 +7,15 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"bitbucket.org/liamstask/goose/lib/goose"
|
||||
|
||||
mysql "github.com/go-sql-driver/mysql"
|
||||
"github.com/gophish/gophish/auth"
|
||||
"github.com/gophish/gophish/config"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/mattn/go-sqlite3" // Blank import needed to import sqlite3
|
||||
|
@ -24,6 +26,19 @@ var conf *config.Config
|
|||
|
||||
const MaxDatabaseConnectionAttempts int = 10
|
||||
|
||||
// DefaultAdminUsername is the default username for the administrative user
|
||||
const DefaultAdminUsername = "admin"
|
||||
|
||||
// InitialAdminPassword is the environment variable that specifies which
|
||||
// password to use for the initial root login instead of generating one
|
||||
// randomly
|
||||
const InitialAdminPassword = "GOPHISH_INITIAL_ADMIN_PASSWORD"
|
||||
|
||||
// InitialAdminApiToken is the environment variable that specifies the
|
||||
// API token to seed the initial root login instead of generating one
|
||||
// randomly
|
||||
const InitialAdminApiToken = "GOPHISH_INITIAL_ADMIN_API_TOKEN"
|
||||
|
||||
const (
|
||||
CampaignInProgress string = "In progress"
|
||||
CampaignQueued string = "Queued"
|
||||
|
@ -83,8 +98,38 @@ func chooseDBDriver(name, openStr string) goose.DBDriver {
|
|||
return d
|
||||
}
|
||||
|
||||
// Setup initializes the Conn object
|
||||
// It also populates the Gophish Config object
|
||||
func createTemporaryPassword(u *User) error {
|
||||
var temporaryPassword string
|
||||
if envPassword := os.Getenv(InitialAdminPassword); envPassword != "" {
|
||||
temporaryPassword = envPassword
|
||||
} else {
|
||||
// This will result in a 16 character password which could be viewed as an
|
||||
// inconvenience, but it should be ok for now.
|
||||
temporaryPassword = auth.GenerateSecureKey(auth.MinPasswordLength)
|
||||
}
|
||||
hash, err := auth.GeneratePasswordHash(temporaryPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Hash = hash
|
||||
// Anytime a temporary password is created, we will force the user
|
||||
// to change their password
|
||||
u.PasswordChangeRequired = true
|
||||
err = db.Save(u).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Please login with the username admin and the password %s", temporaryPassword)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Setup initializes the database and runs any needed migrations.
|
||||
//
|
||||
// First, it establishes a connection to the database, then runs any migrations
|
||||
// newer than the version the database is on.
|
||||
//
|
||||
// Once the database is up-to-date, we create an admin user (if needed) that
|
||||
// has a randomly generated API key and password.
|
||||
func Setup(c *config.Config) error {
|
||||
// Setup the package-scoped config
|
||||
conf = c
|
||||
|
@ -94,8 +139,6 @@ func Setup(c *config.Config) error {
|
|||
Env: "production",
|
||||
Driver: chooseDBDriver(conf.DBName, conf.DBPath),
|
||||
}
|
||||
abs, _ := filepath.Abs(migrateConf.MigrationsDir)
|
||||
fmt.Println(abs)
|
||||
// Get the latest possible migration
|
||||
latest, err := goose.GetMostRecentDBVersion(migrateConf.MigrationsDir)
|
||||
if err != nil {
|
||||
|
@ -156,6 +199,7 @@ func Setup(c *config.Config) error {
|
|||
}
|
||||
// Create the admin user if it doesn't exist
|
||||
var userCount int64
|
||||
var adminUser User
|
||||
db.Model(&User{}).Count(&userCount)
|
||||
adminRole, err := GetRoleBySlug(RoleAdmin)
|
||||
if err != nil {
|
||||
|
@ -163,14 +207,44 @@ func Setup(c *config.Config) error {
|
|||
return err
|
||||
}
|
||||
if userCount == 0 {
|
||||
initUser := User{
|
||||
Username: "admin",
|
||||
Hash: "$2a$10$IYkPp0.QsM81lYYPrQx6W.U6oQGw7wMpozrKhKAHUBVL4mkm/EvAS", //gophish
|
||||
adminUser := User{
|
||||
Username: DefaultAdminUsername,
|
||||
Role: adminRole,
|
||||
RoleID: adminRole.ID,
|
||||
PasswordChangeRequired: true,
|
||||
}
|
||||
initUser.ApiKey = generateSecureKey()
|
||||
err = db.Save(&initUser).Error
|
||||
|
||||
if envToken := os.Getenv(InitialAdminApiToken); envToken != "" {
|
||||
adminUser.ApiKey = envToken
|
||||
} else {
|
||||
adminUser.ApiKey = auth.GenerateSecureKey(auth.APIKeyLength)
|
||||
}
|
||||
|
||||
err = db.Save(&adminUser).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
// If this is the first time the user is installing Gophish, then we will
|
||||
// generate a temporary password for the admin user.
|
||||
//
|
||||
// We do this here instead of in the block above where the admin is created
|
||||
// since there's the chance the user executes Gophish and has some kind of
|
||||
// error, then tries restarting it. If they didn't grab the password out of
|
||||
// the logs, then they would have lost it.
|
||||
//
|
||||
// By doing the temporary password here, we will regenerate that temporary
|
||||
// password until the user is able to reset the admin password.
|
||||
if adminUser.Username == "" {
|
||||
adminUser, err = GetUserByUsername(DefaultAdminUsername)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if adminUser.PasswordChangeRequired {
|
||||
err = createTemporaryPassword(&adminUser)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
|
|
|
@ -138,6 +138,9 @@ func PostPage(p *Page) error {
|
|||
// Per the PUT Method RFC, it presumes all data for a page is provided.
|
||||
func PutPage(p *Page) error {
|
||||
err := p.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.Where("id=?", p.Id).Save(p).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
|
|
@ -19,6 +19,7 @@ type User struct {
|
|||
ApiKey string `json:"api_key" sql:"not null;unique"`
|
||||
Role Role `json:"role" gorm:"association_autoupdate:false;association_autocreate:false"`
|
||||
RoleID int64 `json:"-"`
|
||||
PasswordChangeRequired bool `json:"password_change_required"`
|
||||
}
|
||||
|
||||
// GetUser returns the user that the given id corresponds to. If no user is found, an
|
||||
|
|
|
@ -27,7 +27,9 @@ func (s *ModelsSuite) TestGetUserByAPIKeyWithExistingAPIKey(c *check.C) {
|
|||
u, err := GetUser(1)
|
||||
c.Assert(err, check.Equals, nil)
|
||||
|
||||
u, err = GetUserByAPIKey(u.ApiKey)
|
||||
got, err := GetUserByAPIKey(u.ApiKey)
|
||||
c.Assert(err, check.Equals, nil)
|
||||
c.Assert(got.Id, check.Equals, u.Id)
|
||||
}
|
||||
|
||||
func (s *ModelsSuite) TestGetUserByAPIKeyWithNotExistingAPIKey(c *check.C) {
|
||||
|
@ -46,11 +48,12 @@ func (s *ModelsSuite) TestGetUserByUsernameWithNotExistingUser(c *check.C) {
|
|||
}
|
||||
|
||||
func (s *ModelsSuite) TestPutUser(c *check.C) {
|
||||
u, err := GetUser(1)
|
||||
u, _ := GetUser(1)
|
||||
u.Username = "admin_changed"
|
||||
err = PutUser(&u)
|
||||
err := PutUser(&u)
|
||||
c.Assert(err, check.Equals, nil)
|
||||
u, err = GetUser(1)
|
||||
c.Assert(err, check.Equals, nil)
|
||||
c.Assert(u.Username, check.Equals, "admin_changed")
|
||||
}
|
||||
|
||||
|
|
|
@ -29,5 +29,8 @@
|
|||
"jshint-stylish": "^2.2.1",
|
||||
"webpack": "^4.32.2",
|
||||
"webpack-cli": "^3.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"zxcvbn": "^4.4.2"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -730,3 +730,15 @@ table.dataTable {
|
|||
.cke_autocomplete_panel>li {
|
||||
padding: 10px 5px !important;
|
||||
}
|
||||
|
||||
#password-strength {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 0px;
|
||||
height: 8px;
|
||||
}
|
||||
#password-strength-description {
|
||||
font-size: 12px;
|
||||
}
|
||||
#password-strength-container {
|
||||
height: 40px;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
var groups=[];function save(e){var t=[];$.each($("#targetsTable").DataTable().rows().data(),function(e,a){t.push({first_name:unescapeHtml(a[0]),last_name:unescapeHtml(a[1]),email:unescapeHtml(a[2]),position:unescapeHtml(a[3])})});var a={name:$("#name").val(),targets:t};-1!=e?(a.id=e,api.groupId.put(a).success(function(e){successFlash("Group updated successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})):api.groups.post(a).success(function(e){successFlash("Group added successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})}function dismiss(){$("#targetsTable").dataTable().DataTable().clear().draw(),$("#name").val(""),$("#modal\\.flashes").empty()}function edit(e){if(targets=$("#targetsTable").dataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),$("#modalSubmit").unbind("click").click(function(){save(e)}),-1==e);else api.groupId.get(e).success(function(e){$("#name").val(e.name),$.each(e.targets,function(e,a){targets.DataTable().row.add([escapeHtml(a.first_name),escapeHtml(a.last_name),escapeHtml(a.email),escapeHtml(a.position),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>']).draw()})}).error(function(){errorFlash("Error fetching group")});$("#csvupload").fileupload({url:"/api/import/group",dataType:"json",beforeSend:function(e){e.setRequestHeader("Authorization","Bearer "+user.api_key)},add:function(e,a){$("#modal\\.flashes").empty();var t=a.originalFiles[0].name;if(t&&!/(csv|txt)$/i.test(t.split(".").pop()))return modalError("Unsupported file extension (use .csv or .txt)"),!1;a.submit()},done:function(e,a){$.each(a.result,function(e,a){addTarget(a.first_name,a.last_name,a.email,a.position)}),targets.DataTable().draw()}})}var downloadCSVTemplate=function(){var e="group_template.csv",a=Papa.unparse([{"First Name":"Example","Last Name":"User",Email:"foobar@example.com",Position:"Systems Administrator"}],{}),t=new Blob([a],{type:"text/csv;charset=utf-8;"});if(navigator.msSaveBlob)navigator.msSaveBlob(t,e);else{var o=window.URL.createObjectURL(t),s=document.createElement("a");s.href=o,s.setAttribute("download",e),document.body.appendChild(s),s.click(),document.body.removeChild(s)}},deleteGroup=function(o){var e=groups.find(function(e){return e.id===o});e&&Swal.fire({title:"Are you sure?",text:"This will delete the group. This can't be undone!",type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete "+escapeHtml(e.name),confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(a,t){api.groupId.delete(o).success(function(e){a()}).error(function(e){t(e.responseJSON.message)})})}}).then(function(e){e.value&&Swal.fire("Group Deleted!","This group has been deleted!","success"),$('button:contains("OK")').on("click",function(){location.reload()})})};function addTarget(e,a,t,o){var s=escapeHtml(t).toLowerCase(),r=[escapeHtml(e),escapeHtml(a),s,escapeHtml(o),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'],n=targets.DataTable(),i=n.column(2,{order:"index"}).data().indexOf(s);0<=i?n.row(i,{order:"index"}).data(r):n.row.add(r)}function load(){$("#groupTable").hide(),$("#emptyMessage").hide(),$("#loading").show(),api.groups.summary().success(function(e){if($("#loading").hide(),0<e.total){groups=e.groups,$("#emptyMessage").hide(),$("#groupTable").show();var t=$("#groupTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});t.clear(),$.each(groups,function(e,a){t.row.add([escapeHtml(a.name),escapeHtml(a.num_targets),moment(a.modified_date).format("MMMM Do YYYY, h:mm:ss a"),"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit("+a.id+")'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger' onclick='deleteGroup("+a.id+")'> <i class='fa fa-trash-o'></i> </button></div>"]).draw()})}else $("#emptyMessage").show()}).error(function(){errorFlash("Error fetching groups")})}$(document).ready(function(){load(),$("#targetForm").submit(function(){var e=document.getElementById("targetForm");if(e.checkValidity())return addTarget($("#firstName").val(),$("#lastName").val(),$("#email").val(),$("#position").val()),targets.DataTable().draw(),$("#targetForm>div>input").val(""),$("#firstName").focus(),!1;e.reportValidity()}),$("#targetsTable").on("click","span>i.fa-trash-o",function(){targets.DataTable().row($(this).parents("tr")).remove().draw()}),$("#modal").on("hide.bs.modal",function(){dismiss()}),$("#csv-template").click(downloadCSVTemplate)});
|
||||
var groups=[];function save(e){var t=[];$.each($("#targetsTable").DataTable().rows().data(),function(e,a){t.push({first_name:unescapeHtml(a[0]),last_name:unescapeHtml(a[1]),email:unescapeHtml(a[2]),position:unescapeHtml(a[3])})});var a={name:$("#name").val(),targets:t};-1!=e?(a.id=e,api.groupId.put(a).success(function(e){successFlash("Group updated successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})):api.groups.post(a).success(function(e){successFlash("Group added successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})}function dismiss(){$("#targetsTable").dataTable().DataTable().clear().draw(),$("#name").val(""),$("#modal\\.flashes").empty()}function edit(e){if(targets=$("#targetsTable").dataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),$("#modalSubmit").unbind("click").click(function(){save(e)}),-1==e);else api.groupId.get(e).success(function(e){$("#name").val(e.name),targetRows=[],$.each(e.targets,function(e,a){targetRows.push([escapeHtml(a.first_name),escapeHtml(a.last_name),escapeHtml(a.email),escapeHtml(a.position),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'])}),targets.DataTable().rows.add(targetRows).draw()}).error(function(){errorFlash("Error fetching group")});$("#csvupload").fileupload({url:"/api/import/group",dataType:"json",beforeSend:function(e){e.setRequestHeader("Authorization","Bearer "+user.api_key)},add:function(e,a){$("#modal\\.flashes").empty();var t=a.originalFiles[0].name;if(t&&!/(csv|txt)$/i.test(t.split(".").pop()))return modalError("Unsupported file extension (use .csv or .txt)"),!1;a.submit()},done:function(e,a){$.each(a.result,function(e,a){addTarget(a.first_name,a.last_name,a.email,a.position)}),targets.DataTable().draw()}})}var downloadCSVTemplate=function(){var e="group_template.csv",a=Papa.unparse([{"First Name":"Example","Last Name":"User",Email:"foobar@example.com",Position:"Systems Administrator"}],{}),t=new Blob([a],{type:"text/csv;charset=utf-8;"});if(navigator.msSaveBlob)navigator.msSaveBlob(t,e);else{var o=window.URL.createObjectURL(t),s=document.createElement("a");s.href=o,s.setAttribute("download",e),document.body.appendChild(s),s.click(),document.body.removeChild(s)}},deleteGroup=function(o){var e=groups.find(function(e){return e.id===o});e&&Swal.fire({title:"Are you sure?",text:"This will delete the group. This can't be undone!",type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete "+escapeHtml(e.name),confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(a,t){api.groupId.delete(o).success(function(e){a()}).error(function(e){t(e.responseJSON.message)})})}}).then(function(e){e.value&&Swal.fire("Group Deleted!","This group has been deleted!","success"),$('button:contains("OK")').on("click",function(){location.reload()})})};function addTarget(e,a,t,o){var s=escapeHtml(t).toLowerCase(),r=[escapeHtml(e),escapeHtml(a),s,escapeHtml(o),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'],n=targets.DataTable(),i=n.column(2,{order:"index"}).data().indexOf(s);0<=i?n.row(i,{order:"index"}).data(r):n.row.add(r)}function load(){$("#groupTable").hide(),$("#emptyMessage").hide(),$("#loading").show(),api.groups.summary().success(function(e){if($("#loading").hide(),0<e.total){groups=e.groups,$("#emptyMessage").hide(),$("#groupTable").show();var a=$("#groupTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});a.clear(),groupRows=[],$.each(groups,function(e,a){groupRows.push([escapeHtml(a.name),escapeHtml(a.num_targets),moment(a.modified_date).format("MMMM Do YYYY, h:mm:ss a"),"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit("+a.id+")'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger' onclick='deleteGroup("+a.id+")'> <i class='fa fa-trash-o'></i> </button></div>"])}),a.rows.add(groupRows).draw()}else $("#emptyMessage").show()}).error(function(){errorFlash("Error fetching groups")})}$(document).ready(function(){load(),$("#targetForm").submit(function(){var e=document.getElementById("targetForm");if(e.checkValidity())return addTarget($("#firstName").val(),$("#lastName").val(),$("#email").val(),$("#position").val()),targets.DataTable().draw(),$("#targetForm>div>input").val(""),$("#firstName").focus(),!1;e.reportValidity()}),$("#targetsTable").on("click","span>i.fa-trash-o",function(){targets.DataTable().row($(this).parents("tr")).remove().draw()}),$("#modal").on("hide.bs.modal",function(){dismiss()}),$("#csv-template").click(downloadCSVTemplate)});
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
!function(e){var o={};function n(t){if(o[t])return o[t].exports;var a=o[t]={i:t,l:!1,exports:{}};return e[t].call(a.exports,a,a.exports,n),a.l=!0,a.exports}n.m=e,n.c=o,n.d=function(e,o,t){n.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:t})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,o){if(1&o&&(e=n(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(n.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var a in e)n.d(t,a,function(o){return e[o]}.bind(null,a));return t},n.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(o,"a",o),o},n.o=function(e,o){return Object.prototype.hasOwnProperty.call(e,o)},n.p="",n(n.s=1)}([,function(e,o){var n=[],t=function(){$("#name").val(""),$("#url").val(""),$("#secret").val(""),$("#is_active").prop("checked",!1),$("#flashes").empty()},a=function(){$("#webhookTable").hide(),$("#loading").show(),api.webhooks.get().success(function(e){n=e,$("#loading").hide(),$("#webhookTable").show();var o=$("#webhookTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});o.clear(),$.each(n,function(e,n){o.row.add([escapeHtml(n.name),escapeHtml(n.url),escapeHtml(n.is_active),'\n <div class="pull-right">\n <button class="btn btn-primary ping_button" data-webhook-id="'.concat(n.id,'">\n Ping\n </button>\n <button class="btn btn-primary edit_button" data-toggle="modal" data-backdrop="static" data-target="#modal" data-webhook-id="').concat(n.id,'">\n <i class="fa fa-pencil"></i>\n </button>\n <button class="btn btn-danger delete_button" data-webhook-id="').concat(n.id,'">\n <i class="fa fa-trash-o"></i>\n </button>\n </div>\n ')]).draw()})}).error(function(){errorFlash("Error fetching webhooks")})},c=function(e){$("#modalSubmit").unbind("click").click(function(){!function(e){var o={name:$("#name").val(),url:$("#url").val(),secret:$("#secret").val(),is_active:$("#is_active").is(":checked")};-1!=e?(o.id=e,api.webhookId.put(o).success(function(e){t(),a(),$("#modal").modal("hide"),successFlash('Webhook "'.concat(escape(o.name),'" has been updated successfully!'))}).error(function(e){modalError(e.responseJSON.message)})):api.webhooks.post(o).success(function(e){a(),t(),$("#modal").modal("hide"),successFlash('Webhook "'.concat(escape(o.name),'" has been created successfully!'))}).error(function(e){modalError(e.responseJSON.message)})}(e)}),-1!==e&&api.webhookId.get(e).success(function(e){$("#name").val(e.name),$("#url").val(e.url),$("#secret").val(e.secret),$("#is_active").prop("checked",e.is_active)}).error(function(){errorFlash("Error fetching webhook")})};$(document).ready(function(){a(),$("#modal").on("hide.bs.modal",function(){t()}),$("#new_button").on("click",function(){c(-1)}),$("#webhookTable").on("click",".edit_button",function(e){c($(this).attr("data-webhook-id"))}),$("#webhookTable").on("click",".delete_button",function(e){var o,t;o=$(this).attr("data-webhook-id"),(t=n.find(function(e){return e.id==o}))&&Swal.fire({title:"Are you sure?",text:"This will delete the webhook '".concat(escape(t.name),"'"),type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete",confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(e,n){api.webhookId.delete(o).success(function(o){e()}).error(function(e){n(e.responseJSON.message)})}).catch(function(e){Swal.showValidationMessage(e)})}}).then(function(e){e.value&&Swal.fire("Webhook Deleted!","The webhook has been deleted!","success"),$("button:contains('OK')").on("click",function(){location.reload()})})}),$("#webhookTable").on("click",".ping_button",function(e){var o,a;o=e.currentTarget,a=e.currentTarget.dataset.webhookId,t(),o.disabled=!0,api.webhookId.ping(a).success(function(e){o.disabled=!1,successFlash('Ping of "'.concat(escape(e.name),'" webhook succeeded.'))}).error(function(e){o.disabled=!1;var t=n.find(function(e){return e.id==a});t&&errorFlash('Ping of "'.concat(escape(t.name),'" webhook failed: "').concat(e.responseJSON.message,'"'))})})})}]);
|
||||
!function(e){var t={};function n(o){if(t[o])return t[o].exports;var a=t[o]={i:o,l:!1,exports:{}};return e[o].call(a.exports,a,a.exports,n),a.l=!0,a.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)n.d(o,a,function(t){return e[t]}.bind(null,a));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=9)}({9:function(e,t){var n=[],o=function(){$("#name").val(""),$("#url").val(""),$("#secret").val(""),$("#is_active").prop("checked",!1),$("#flashes").empty()},a=function(){$("#webhookTable").hide(),$("#loading").show(),api.webhooks.get().success(function(e){n=e,$("#loading").hide(),$("#webhookTable").show();var t=$("#webhookTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});t.clear(),$.each(n,function(e,n){t.row.add([escapeHtml(n.name),escapeHtml(n.url),escapeHtml(n.is_active),'\n <div class="pull-right">\n <button class="btn btn-primary ping_button" data-webhook-id="'.concat(n.id,'">\n Ping\n </button>\n <button class="btn btn-primary edit_button" data-toggle="modal" data-backdrop="static" data-target="#modal" data-webhook-id="').concat(n.id,'">\n <i class="fa fa-pencil"></i>\n </button>\n <button class="btn btn-danger delete_button" data-webhook-id="').concat(n.id,'">\n <i class="fa fa-trash-o"></i>\n </button>\n </div>\n ')]).draw()})}).error(function(){errorFlash("Error fetching webhooks")})},c=function(e){$("#modalSubmit").unbind("click").click(function(){!function(e){var t={name:$("#name").val(),url:$("#url").val(),secret:$("#secret").val(),is_active:$("#is_active").is(":checked")};-1!=e?(t.id=parseInt(e),api.webhookId.put(t).success(function(e){o(),a(),$("#modal").modal("hide"),successFlash('Webhook "'.concat(escapeHtml(t.name),'" has been updated successfully!'))}).error(function(e){modalError(e.responseJSON.message)})):api.webhooks.post(t).success(function(e){a(),o(),$("#modal").modal("hide"),successFlash('Webhook "'.concat(escapeHtml(t.name),'" has been created successfully!'))}).error(function(e){modalError(e.responseJSON.message)})}(e)}),-1!==e&&api.webhookId.get(e).success(function(e){$("#name").val(e.name),$("#url").val(e.url),$("#secret").val(e.secret),$("#is_active").prop("checked",e.is_active)}).error(function(){errorFlash("Error fetching webhook")})};$(document).ready(function(){a(),$("#modal").on("hide.bs.modal",function(){o()}),$("#new_button").on("click",function(){c(-1)}),$("#webhookTable").on("click",".edit_button",function(e){c($(this).attr("data-webhook-id"))}),$("#webhookTable").on("click",".delete_button",function(e){var t,o;t=$(this).attr("data-webhook-id"),(o=n.find(function(e){return e.id==t}))&&Swal.fire({title:"Are you sure?",text:"This will delete the webhook '".concat(escapeHtml(o.name),"'"),type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete",confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(e,n){api.webhookId.delete(t).success(function(t){e()}).error(function(e){n(e.responseJSON.message)})}).catch(function(e){Swal.showValidationMessage(e)})}}).then(function(e){e.value&&Swal.fire("Webhook Deleted!","The webhook has been deleted!","success"),$("button:contains('OK')").on("click",function(){location.reload()})})}),$("#webhookTable").on("click",".ping_button",function(e){var t,a;t=e.currentTarget,a=e.currentTarget.dataset.webhookId,o(),t.disabled=!0,api.webhookId.ping(a).success(function(e){t.disabled=!1,successFlash('Ping of "'.concat(escapeHtml(e.name),'" webhook succeeded.'))}).error(function(e){t.disabled=!1;var o=n.find(function(e){return e.id==a});o&&errorFlash('Ping of "'.concat(escapeHtml(o.name),'" webhook failed: "').concat(e.responseJSON.message,'"'))})})})}});
|
|
@ -162,15 +162,16 @@ function deleteCampaign(idx) {
|
|||
}
|
||||
|
||||
function setupOptions() {
|
||||
api.groups.get()
|
||||
.success(function (groups) {
|
||||
api.groups.summary()
|
||||
.success(function (summaries) {
|
||||
groups = summaries.groups
|
||||
if (groups.length == 0) {
|
||||
modalError("No groups found!")
|
||||
return false;
|
||||
} else {
|
||||
var group_s2 = $.map(groups, function (obj) {
|
||||
obj.text = obj.name
|
||||
obj.title = obj.targets.length + " targets"
|
||||
obj.title = obj.num_targets + " targets"
|
||||
return obj
|
||||
});
|
||||
console.log(group_s2)
|
||||
|
@ -360,6 +361,7 @@ $(document).ready(function () {
|
|||
[1, "desc"]
|
||||
]
|
||||
});
|
||||
campaignRows = []
|
||||
$.each(campaigns, function (i, campaign) {
|
||||
campaignTable = campaignTableOriginal
|
||||
if (campaign.status === "Completed") {
|
||||
|
@ -378,7 +380,7 @@ $(document).ready(function () {
|
|||
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error + "Reported : " + campaign.stats.reported
|
||||
}
|
||||
|
||||
campaignTable.row.add([
|
||||
campaignRows.push([
|
||||
escapeHtml(campaign.name),
|
||||
moment(campaign.created_date).format('MMMM Do YYYY, h:mm:ss a'),
|
||||
"<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"right\" data-html=\"true\" title=\"" + quickStats + "\">" + campaign.status + "</span>",
|
||||
|
@ -391,9 +393,10 @@ $(document).ready(function () {
|
|||
<button class='btn btn-danger' onclick='deleteCampaign(" + i + ")' data-toggle='tooltip' data-placement='left' title='Delete Campaign'>\
|
||||
<i class='fa fa-trash-o'></i>\
|
||||
</button></div>"
|
||||
]).draw()
|
||||
])
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
})
|
||||
campaignTable.rows.add(campaignRows).draw()
|
||||
} else {
|
||||
$("#emptyMessage").show()
|
||||
}
|
||||
|
|
|
@ -326,6 +326,7 @@ $(document).ready(function () {
|
|||
[1, "desc"]
|
||||
]
|
||||
});
|
||||
campaignRows = []
|
||||
$.each(campaigns, function (i, campaign) {
|
||||
var campaign_date = moment(campaign.created_date).format('MMMM Do YYYY, h:mm:ss a')
|
||||
var label = statuses[campaign.status].label || "label-default";
|
||||
|
@ -338,8 +339,8 @@ $(document).ready(function () {
|
|||
launchDate = "Launch Date: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
|
||||
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error + "<br><br>" + "Reported : " + campaign.stats.email_reported
|
||||
}
|
||||
// Add it to the table
|
||||
campaignTable.row.add([
|
||||
// Add it to the list
|
||||
campaignRows.push([
|
||||
escapeHtml(campaign.name),
|
||||
campaign_date,
|
||||
campaign.stats.sent,
|
||||
|
@ -354,9 +355,10 @@ $(document).ready(function () {
|
|||
<button class='btn btn-danger' onclick='deleteCampaign(" + i + ")' data-toggle='tooltip' data-placement='left' title='Delete Campaign'>\
|
||||
<i class='fa fa-trash-o'></i>\
|
||||
</button></div>"
|
||||
]).draw()
|
||||
])
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
})
|
||||
campaignTable.rows.add(campaignRows).draw()
|
||||
// Build the charts
|
||||
generateStatsPieCharts(campaigns)
|
||||
generateTimelineChart(campaigns)
|
||||
|
|
|
@ -69,17 +69,17 @@ function edit(id) {
|
|||
api.groupId.get(id)
|
||||
.success(function (group) {
|
||||
$("#name").val(group.name)
|
||||
targetRows = []
|
||||
$.each(group.targets, function (i, record) {
|
||||
targets.DataTable()
|
||||
.row.add([
|
||||
targetRows.push([
|
||||
escapeHtml(record.first_name),
|
||||
escapeHtml(record.last_name),
|
||||
escapeHtml(record.email),
|
||||
escapeHtml(record.position),
|
||||
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
||||
]).draw()
|
||||
])
|
||||
});
|
||||
|
||||
targets.DataTable().rows.add(targetRows).draw()
|
||||
})
|
||||
.error(function () {
|
||||
errorFlash("Error fetching group")
|
||||
|
@ -233,8 +233,9 @@ function load() {
|
|||
}]
|
||||
});
|
||||
groupTable.clear();
|
||||
groupRows = []
|
||||
$.each(groups, function (i, group) {
|
||||
groupTable.row.add([
|
||||
groupRows.push([
|
||||
escapeHtml(group.name),
|
||||
escapeHtml(group.num_targets),
|
||||
moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
|
||||
|
@ -244,8 +245,9 @@ function load() {
|
|||
<button class='btn btn-danger' onclick='deleteGroup(" + group.id + ")'>\
|
||||
<i class='fa fa-trash-o'></i>\
|
||||
</button></div>"
|
||||
]).draw()
|
||||
])
|
||||
})
|
||||
groupTable.rows.add(groupRows).draw()
|
||||
} else {
|
||||
$("#emptyMessage").show()
|
||||
}
|
||||
|
|
|
@ -157,8 +157,9 @@ function load() {
|
|||
}]
|
||||
});
|
||||
pagesTable.clear()
|
||||
pageRows = []
|
||||
$.each(pages, function (i, page) {
|
||||
pagesTable.row.add([
|
||||
pageRows.push([
|
||||
escapeHtml(page.name),
|
||||
moment(page.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
|
||||
"<div class='pull-right'><span data-toggle='modal' data-backdrop='static' data-target='#modal'><button class='btn btn-primary' data-toggle='tooltip' data-placement='left' title='Edit Page' onclick='edit(" + i + ")'>\
|
||||
|
@ -170,8 +171,9 @@ function load() {
|
|||
<button class='btn btn-danger' data-toggle='tooltip' data-placement='left' title='Delete Page' onclick='deletePage(" + i + ")'>\
|
||||
<i class='fa fa-trash-o'></i>\
|
||||
</button></div>"
|
||||
]).draw()
|
||||
])
|
||||
})
|
||||
pagesTable.rows.add(pageRows).draw()
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
} else {
|
||||
$("#emptyMessage").show()
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import zxcvbn from 'zxcvbn';
|
||||
|
||||
const StrengthMapping = {
|
||||
0: {
|
||||
class: 'danger',
|
||||
width: '10%',
|
||||
status: 'Very Weak'
|
||||
},
|
||||
1: {
|
||||
class: 'danger',
|
||||
width: '25%',
|
||||
status: 'Very Weak'
|
||||
},
|
||||
2: {
|
||||
class: 'warning',
|
||||
width: '50%',
|
||||
status: 'Weak'
|
||||
},
|
||||
3: {
|
||||
class: 'success',
|
||||
width: '75%',
|
||||
status: 'Good'
|
||||
},
|
||||
4: {
|
||||
class: 'success',
|
||||
width: '100%',
|
||||
status: 'Very Good'
|
||||
}
|
||||
}
|
||||
|
||||
const Progress = document.getElementById("password-strength-container")
|
||||
const ProgressBar = document.getElementById("password-strength-bar")
|
||||
const StrengthDescription = document.getElementById("password-strength-description")
|
||||
|
||||
const updatePasswordStrength = (e) => {
|
||||
const candidate = e.target.value
|
||||
// If there is no password, clear out the progress bar
|
||||
if (!candidate) {
|
||||
ProgressBar.style.width = 0
|
||||
StrengthDescription.textContent = ""
|
||||
Progress.classList.add("hidden")
|
||||
return
|
||||
}
|
||||
const score = zxcvbn(candidate).score
|
||||
const evaluation = StrengthMapping[score]
|
||||
// Update the progress bar
|
||||
ProgressBar.classList = `progress-bar progress-bar-${evaluation.class}`
|
||||
ProgressBar.style.width = evaluation.width
|
||||
StrengthDescription.textContent = evaluation.status
|
||||
StrengthDescription.classList = `text-${evaluation.class}`
|
||||
Progress.classList.remove("hidden")
|
||||
}
|
||||
|
||||
document.getElementById("password").addEventListener("input", updatePasswordStrength)
|
|
@ -200,8 +200,9 @@ function load() {
|
|||
}]
|
||||
});
|
||||
profileTable.clear()
|
||||
profileRows = []
|
||||
$.each(profiles, function (i, profile) {
|
||||
profileTable.row.add([
|
||||
profileRows.push([
|
||||
escapeHtml(profile.name),
|
||||
profile.interface_type,
|
||||
moment(profile.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
|
||||
|
@ -214,8 +215,9 @@ function load() {
|
|||
<button class='btn btn-danger' data-toggle='tooltip' data-placement='left' title='Delete Profile' onclick='deleteProfile(" + i + ")'>\
|
||||
<i class='fa fa-trash-o'></i>\
|
||||
</button></div>"
|
||||
]).draw()
|
||||
])
|
||||
})
|
||||
profileTable.rows.add(profileRows).draw()
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
} else {
|
||||
$("#emptyMessage").show()
|
||||
|
|
|
@ -191,17 +191,19 @@ function edit(idx) {
|
|||
$("#subject").val(template.subject)
|
||||
$("#html_editor").val(template.html)
|
||||
$("#text_editor").val(template.text)
|
||||
attachmentRows = []
|
||||
$.each(template.attachments, function (i, file) {
|
||||
var icon = icons[file.type] || "fa-file-o"
|
||||
// Add the record to the modal
|
||||
attachmentsTable.row.add([
|
||||
attachmentRows.push([
|
||||
'<i class="fa ' + icon + '"></i>',
|
||||
escapeHtml(file.name),
|
||||
'<span class="remove-row"><i class="fa fa-trash-o"></i></span>',
|
||||
file.content,
|
||||
file.type || "application/octet-stream"
|
||||
]).draw()
|
||||
])
|
||||
})
|
||||
attachmentsTable.rows.add(attachmentRows).draw()
|
||||
if (template.html.indexOf("{{.Tracker}}") != -1) {
|
||||
$("#use_tracker_checkbox").prop("checked", true)
|
||||
} else {
|
||||
|
@ -316,8 +318,9 @@ function load() {
|
|||
}]
|
||||
});
|
||||
templateTable.clear()
|
||||
templateRows = []
|
||||
$.each(templates, function (i, template) {
|
||||
templateTable.row.add([
|
||||
templateRows.push([
|
||||
escapeHtml(template.name),
|
||||
moment(template.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
|
||||
"<div class='pull-right'><span data-toggle='modal' data-backdrop='static' data-target='#modal'><button class='btn btn-primary' data-toggle='tooltip' data-placement='left' title='Edit Template' onclick='edit(" + i + ")'>\
|
||||
|
@ -329,8 +332,9 @@ function load() {
|
|||
<button class='btn btn-danger' data-toggle='tooltip' data-placement='left' title='Delete Template' onclick='deleteTemplate(" + i + ")'>\
|
||||
<i class='fa fa-trash-o'></i>\
|
||||
</button></div>"
|
||||
]).draw()
|
||||
])
|
||||
})
|
||||
templateTable.rows.add(templateRows).draw()
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
} else {
|
||||
$("#emptyMessage").show()
|
||||
|
|
|
@ -10,7 +10,8 @@ const save = (id) => {
|
|||
let user = {
|
||||
username: $("#username").val(),
|
||||
password: $("#password").val(),
|
||||
role: $("#role").val()
|
||||
role: $("#role").val(),
|
||||
password_change_required: $("#force_password_change_checkbox").prop('checked')
|
||||
}
|
||||
// Submit the user
|
||||
if (id != -1) {
|
||||
|
@ -18,26 +19,26 @@ const save = (id) => {
|
|||
// we need to PUT /user/:id
|
||||
user.id = id
|
||||
api.userId.put(user)
|
||||
.success(function (data) {
|
||||
.success((data) => {
|
||||
successFlash("User " + escapeHtml(user.username) + " updated successfully!")
|
||||
load()
|
||||
dismiss()
|
||||
$("#modal").modal('hide')
|
||||
})
|
||||
.error(function (data) {
|
||||
.error((data) => {
|
||||
modalError(data.responseJSON.message)
|
||||
})
|
||||
} else {
|
||||
// Else, if this is a new user, POST it
|
||||
// to /user
|
||||
api.users.post(user)
|
||||
.success(function (data) {
|
||||
.success((data) => {
|
||||
successFlash("User " + escapeHtml(user.username) + " registered successfully!")
|
||||
load()
|
||||
dismiss()
|
||||
$("#modal").modal('hide')
|
||||
})
|
||||
.error(function (data) {
|
||||
.error((data) => {
|
||||
modalError(data.responseJSON.message)
|
||||
})
|
||||
}
|
||||
|
@ -61,10 +62,11 @@ const edit = (id) => {
|
|||
$("#role").trigger("change")
|
||||
} else {
|
||||
api.userId.get(id)
|
||||
.success(function (user) {
|
||||
.success((user) => {
|
||||
$("#username").val(user.username)
|
||||
$("#role").val(user.role.slug)
|
||||
$("#role").trigger("change")
|
||||
$("#force_password_change_checkbox").prop('checked', false)
|
||||
})
|
||||
.error(function () {
|
||||
errorFlash("Error fetching user")
|
||||
|
@ -115,6 +117,55 @@ const deleteUser = (id) => {
|
|||
})
|
||||
}
|
||||
|
||||
const impersonate = (id) => {
|
||||
var user = users.find(x => x.id == id)
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
Swal.fire({
|
||||
title: "Are you sure?",
|
||||
html: "You will be logged out of your account and logged in as <strong>" + escapeHtml(user.username) + "</strong>",
|
||||
type: "warning",
|
||||
animation: false,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Swap User",
|
||||
confirmButtonColor: "#428bca",
|
||||
reverseButtons: true,
|
||||
allowOutsideClick: false,
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
|
||||
fetch('/impersonate', {
|
||||
method: 'post',
|
||||
body: "username=" + user.username + "&csrf_token=" + encodeURIComponent(csrf_token),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}).then((response) => {
|
||||
if (response.status == 200) {
|
||||
Swal.fire({
|
||||
title: "Success!",
|
||||
html: "Successfully changed to user <strong>" + escapeHtml(user.username) + "</strong>.",
|
||||
type: "success",
|
||||
showCancelButton: false,
|
||||
confirmButtonText: "Home",
|
||||
allowOutsideClick: false,
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
window.location.href = "/"
|
||||
}});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Error!",
|
||||
type: "error",
|
||||
html: "Failed to change to user <strong>" + escapeHtml(user.username) + "</strong>.",
|
||||
showCancelButton: false,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const load = () => {
|
||||
$("#userTable").hide()
|
||||
|
@ -132,18 +183,24 @@ const load = () => {
|
|||
}]
|
||||
});
|
||||
userTable.clear();
|
||||
userRows = []
|
||||
$.each(users, (i, user) => {
|
||||
userTable.row.add([
|
||||
userRows.push([
|
||||
escapeHtml(user.username),
|
||||
escapeHtml(user.role.name),
|
||||
"<div class='pull-right'><button class='btn btn-primary edit_button' data-toggle='modal' data-backdrop='static' data-target='#modal' data-user-id='" + user.id + "'>\
|
||||
"<div class='pull-right'>\
|
||||
<button class='btn btn-warning impersonate_button' data-user-id='" + user.id + "'>\
|
||||
<i class='fa fa-retweet'></i>\
|
||||
</button>\
|
||||
<button class='btn btn-primary edit_button' data-toggle='modal' data-backdrop='static' data-target='#modal' data-user-id='" + user.id + "'>\
|
||||
<i class='fa fa-pencil'></i>\
|
||||
</button>\
|
||||
<button class='btn btn-danger delete_button' data-user-id='" + user.id + "'>\
|
||||
<i class='fa fa-trash-o'></i>\
|
||||
</button></div>"
|
||||
]).draw()
|
||||
])
|
||||
})
|
||||
userTable.rows.add(userRows).draw();
|
||||
})
|
||||
.error(() => {
|
||||
errorFlash("Error fetching users")
|
||||
|
@ -180,4 +237,7 @@ $(document).ready(function () {
|
|||
$("#userTable").on('click', '.delete_button', function (e) {
|
||||
deleteUser($(this).attr('data-user-id'))
|
||||
})
|
||||
$("#userTable").on('click', '.impersonate_button', function (e) {
|
||||
impersonate($(this).attr('data-user-id'))
|
||||
})
|
||||
});
|
||||
|
|
|
@ -16,13 +16,13 @@ const saveWebhook = (id) => {
|
|||
is_active: $("#is_active").is(":checked"),
|
||||
};
|
||||
if (id != -1) {
|
||||
wh.id = id;
|
||||
wh.id = parseInt(id);
|
||||
api.webhookId.put(wh)
|
||||
.success(function(data) {
|
||||
dismiss();
|
||||
load();
|
||||
$("#modal").modal("hide");
|
||||
successFlash(`Webhook "${escape(wh.name)}" has been updated successfully!`);
|
||||
successFlash(`Webhook "${escapeHtml(wh.name)}" has been updated successfully!`);
|
||||
})
|
||||
.error(function(data) {
|
||||
modalError(data.responseJSON.message)
|
||||
|
@ -33,7 +33,7 @@ const saveWebhook = (id) => {
|
|||
load();
|
||||
dismiss();
|
||||
$("#modal").modal("hide");
|
||||
successFlash(`Webhook "${escape(wh.name)}" has been created successfully!`);
|
||||
successFlash(`Webhook "${escapeHtml(wh.name)}" has been created successfully!`);
|
||||
})
|
||||
.error(function(data) {
|
||||
modalError(data.responseJSON.message)
|
||||
|
@ -108,7 +108,7 @@ const deleteWebhook = (id) => {
|
|||
}
|
||||
Swal.fire({
|
||||
title: "Are you sure?",
|
||||
text: `This will delete the webhook '${escape(wh.name)}'`,
|
||||
text: `This will delete the webhook '${escapeHtml(wh.name)}'`,
|
||||
type: "warning",
|
||||
animation: false,
|
||||
showCancelButton: true,
|
||||
|
@ -150,7 +150,7 @@ const pingUrl = (btn, whId) => {
|
|||
api.webhookId.ping(whId)
|
||||
.success(function(wh) {
|
||||
btn.disabled = false;
|
||||
successFlash(`Ping of "${escape(wh.name)}" webhook succeeded.`);
|
||||
successFlash(`Ping of "${escapeHtml(wh.name)}" webhook succeeded.`);
|
||||
})
|
||||
.error(function(data) {
|
||||
btn.disabled = false;
|
||||
|
@ -158,7 +158,7 @@ const pingUrl = (btn, whId) => {
|
|||
if (!wh) {
|
||||
return
|
||||
}
|
||||
errorFlash(`Ping of "${escape(wh.name)}" webhook failed: "${data.responseJSON.message}"`)
|
||||
errorFlash(`Ping of "${escapeHtml(wh.name)}" webhook failed: "${data.responseJSON.message}"`)
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Gophish - Phishing Toolkit">
|
||||
<meta name="author" content="Jordan Wright (http://github.com/jordan-wright)">
|
||||
<link rel="shortcut icon" href="../../docs-assets/ico/favicon.png">
|
||||
<link rel="shortcut icon" href="/images/favicon.ico">
|
||||
|
||||
<title>{{ .Title }} - Gophish</title>
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
<div id="loading">
|
||||
<i class="fa fa-spinner fa-spin fa-4x"></i>
|
||||
</div>
|
||||
<div id="emptyMessage" class="row" style="display:none;">
|
||||
{{template "flashes" .Flashes}}
|
||||
<div id="emptyMessage" style="display:none;">
|
||||
<div class="alert alert-info">
|
||||
No campaigns created yet. Let's create one!
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
fa-exclamation-triangle
|
||||
{{else if eq .Type "success"}}
|
||||
fa-check-circle
|
||||
{{else if eq .Type "info"}}
|
||||
fa-info-circle
|
||||
{{end}}"></i>
|
||||
{{.Message}}
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Gophish - Open-Source Phishing Toolkit">
|
||||
<meta name="author" content="Jordan Wright (http://github.com/jordan-wright)">
|
||||
<link rel="shortcut icon" href="../../docs-assets/ico/favicon.png">
|
||||
<link rel="shortcut icon" href="/images/favicon.ico">
|
||||
|
||||
<title>Gophish - {{ .Title }}</title>
|
||||
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
{{ define "base" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Gophish - Open-Source Phishing Toolkit">
|
||||
<meta name="author" content="Jordan Wright (http://github.com/jordan-wright)">
|
||||
<link rel="shortcut icon" href="../../docs-assets/ico/favicon.png">
|
||||
|
||||
<title>Gophish - {{ .Title }}</title>
|
||||
|
||||
<link href="/css/dist/gophish.css" rel="stylesheet">
|
||||
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,600,700' rel='stylesheet'
|
||||
type='text/css'>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<img class="navbar-logo" src="/images/logo_inv_small.png" />
|
||||
<a class="navbar-brand" href="/"> gophish</a>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li>
|
||||
<div class="btn-group" id="navbar-dropdown">
|
||||
<a class="btn btn-primary" href="/logout"><i class="fa fa-user"></i> {{.User.Username}}</a>
|
||||
<a class="btn btn-primary dropdown-toggle" href="/logout">
|
||||
<i class="fa fa-sign-out"></i>
|
||||
</a>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<form class="form-signin" action="" method="POST">
|
||||
<img id="logo" src="/images/logo_purple.png" />
|
||||
<h2 class="form-signin-heading">Reset Your Password</h2>
|
||||
{{template "flashes" .Flashes}}
|
||||
<input type="password" id="password" name="password" class="form-control" placeholder="Password"
|
||||
autocomplete="off" minlength="8" required autofocus>
|
||||
<div class="" id="password-strength-container">
|
||||
<div class="progress" id="password-strength">
|
||||
<div id="password-strength-bar" class="progress-bar" role="progressbar" aria-valuenow="0"
|
||||
aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<span id="password-strength-description"></span>
|
||||
</div>
|
||||
<input type="password" name="confirm_password" class="form-control" placeholder="Confirm Password"
|
||||
autocomplete="off" minlength="8" required>
|
||||
<input type="hidden" name="csrf_token" value="{{.Token}}" />
|
||||
<br />
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Save Password</button>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Placed at the end of the document so the pages load faster -->
|
||||
<script src="/js/dist/app/passwords.min.js"></script>
|
||||
<script src="/js/dist/vendor.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{ end }}
|
|
@ -4,6 +4,7 @@
|
|||
<h1 class="page-header">Settings</h1>
|
||||
</div>
|
||||
<div id="flashes" class="row"></div>
|
||||
{{template "flashes" .Flashes}}
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="active" role="mainSettings"><a href="#mainSettings" aria-controls="mainSettings" role="tab"
|
||||
|
@ -58,8 +59,15 @@
|
|||
<div class="row">
|
||||
<label for="new_password" class="col-sm-2 control-label form-label">New Password:</label>
|
||||
<div class="col-md-6">
|
||||
<input type="password" id="new_password" name="new_password" autocomplete="off"
|
||||
<input type="password" id="password" name="new_password" autocomplete="new-password"
|
||||
class="form-control" />
|
||||
<div class="hidden" id="password-strength-container">
|
||||
<div class="progress" id="password-strength">
|
||||
<div id="password-strength-bar" class="progress-bar" role="progressbar"
|
||||
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<span id="password-strength-description"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
|
@ -225,5 +233,6 @@
|
|||
</div>
|
||||
</div>
|
||||
{{end}} {{define "scripts"}}
|
||||
<script src="/js/dist/app/passwords.min.js"></script>
|
||||
<script src="/js/dist/app/settings.min.js"></script>
|
||||
{{end}}
|
|
@ -5,7 +5,9 @@
|
|||
{{.Title}}
|
||||
</h1>
|
||||
</div>
|
||||
<div id="flashes" class="row"></div>
|
||||
<div id="flashes" class="row">
|
||||
{{template "flashes" .Flashes}}
|
||||
</div>
|
||||
<div class="row">
|
||||
<button type="button" class="btn btn-primary" id="new_button" data-toggle="modal" data-backdrop="static"
|
||||
data-user-id="-1" data-target="#modal">
|
||||
|
@ -47,13 +49,23 @@
|
|||
</div>
|
||||
<label class="control-label" for="password">Password:</label>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" placeholder="Password" id="password" required />
|
||||
<input type="password" class="form-control" autocomplete="new-password" placeholder="Password" id="password" required />
|
||||
<div class="hidden" id="password-strength-container">
|
||||
<div class="progress" id="password-strength">
|
||||
<div id="password-strength-bar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<span id="password-strength-description"></span>
|
||||
</div>
|
||||
</div>
|
||||
<label class="control-label" for="confirm_password">Confirm Password:</label>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" placeholder="Confirm Password" id="confirm_password"
|
||||
required />
|
||||
</div>
|
||||
<div class="checkbox checkbox-primary">
|
||||
<input id="force_password_change_checkbox" type="checkbox" checked>
|
||||
<label for="force_password_change_checkbox">Require the user to set a new password</label>
|
||||
</div>
|
||||
<label class="control-label" for="role">Role:</label>
|
||||
<div class="form-group" id="role-select">
|
||||
<select class="form-control" placeholder="" id="role" />
|
||||
|
@ -70,5 +82,6 @@
|
|||
</div>
|
||||
</div>
|
||||
{{end}} {{define "scripts"}}
|
||||
<script src="/js/dist/app/passwords.min.js"></script>
|
||||
<script src="/js/dist/app/users.min.js"></script>
|
||||
{{end}}
|
32
util/util.go
32
util/util.go
|
@ -21,7 +21,6 @@ import (
|
|||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/jordan-wright/email"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -138,6 +137,9 @@ func CheckAndCreateSSL(cp string, kp string) error {
|
|||
log.Infof("Creating new self-signed certificates for administration interface")
|
||||
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating tls private key: %v", err)
|
||||
}
|
||||
|
||||
notBefore := time.Now()
|
||||
// Generate a certificate that lasts for 10 years
|
||||
|
@ -147,7 +149,7 @@ func CheckAndCreateSSL(cp string, kp string) error {
|
|||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("TLS Certificate Generation: Failed to generate a random serial number: %s", err)
|
||||
return fmt.Errorf("tls certificate generation: failed to generate a random serial number: %s", err)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
|
@ -165,24 +167,24 @@ func CheckAndCreateSSL(cp string, kp string) error {
|
|||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("TLS Certificate Generation: Failed to create certificate: %s", err)
|
||||
return fmt.Errorf("tls certificate generation: failed to create certificate: %s", err)
|
||||
}
|
||||
|
||||
certOut, err := os.Create(cp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("TLS Certificate Generation: Failed to open %s for writing: %s", cp, err)
|
||||
return fmt.Errorf("tls certificate generation: failed to open %s for writing: %s", cp, err)
|
||||
}
|
||||
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
certOut.Close()
|
||||
|
||||
keyOut, err := os.OpenFile(kp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("TLS Certificate Generation: Failed to open %s for writing", kp)
|
||||
return fmt.Errorf("tls certificate generation: failed to open %s for writing", kp)
|
||||
}
|
||||
|
||||
b, err := x509.MarshalECPrivateKey(priv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("TLS Certificate Generation: Unable to marshal ECDSA private key: %v", err)
|
||||
return fmt.Errorf("tls certificate generation: unable to marshal ECDSA private key: %v", err)
|
||||
}
|
||||
|
||||
pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: b})
|
||||
|
@ -191,21 +193,3 @@ func CheckAndCreateSSL(cp string, kp string) error {
|
|||
log.Info("TLS Certificate Generation complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateSecureKey creates a secure key to use as an API key
|
||||
func GenerateSecureKey() string {
|
||||
// Inspired from gorilla/securecookie
|
||||
k := make([]byte, 32)
|
||||
io.ReadFull(rand.Reader, k)
|
||||
return fmt.Sprintf("%x", k)
|
||||
}
|
||||
|
||||
// NewHash hashes the provided password and returns the bcrypt hash (using the
|
||||
// default 10 rounds) as a string.
|
||||
func NewHash(pass string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
|
|
@ -16,20 +16,24 @@ import (
|
|||
|
||||
const (
|
||||
|
||||
// DefaultTimeoutSeconds is amount of seconds of timeout used by HTTP sender
|
||||
// DefaultTimeoutSeconds is the number of seconds before a timeout occurs
|
||||
// when sending a webhook
|
||||
DefaultTimeoutSeconds = 10
|
||||
|
||||
// MinHTTPStatusErrorCode is the lowest number of an HTTP response which indicates an error
|
||||
// MinHTTPStatusErrorCode is the lower bound of HTTP status codes which
|
||||
// indicate an error occurred
|
||||
MinHTTPStatusErrorCode = 400
|
||||
|
||||
// SignatureHeader is the name of an HTTP header used to which contains signature of a webhook
|
||||
// SignatureHeader is the name of the HTTP header which contains the
|
||||
// webhook signature
|
||||
SignatureHeader = "X-Gophish-Signature"
|
||||
|
||||
// Sha256Prefix is the prefix that specifies the hashing algorithm used for signature
|
||||
// Sha256Prefix is the prefix that specifies the hashing algorithm used
|
||||
// for the signature
|
||||
Sha256Prefix = "sha256"
|
||||
)
|
||||
|
||||
// Sender defines behaviour of an entity by which webhook is sent
|
||||
// Sender represents a type which can send webhooks to an EndPoint
|
||||
type Sender interface {
|
||||
Send(endPoint EndPoint, data interface{}) error
|
||||
}
|
||||
|
@ -47,7 +51,8 @@ var senderInstance = &defaultSender{
|
|||
},
|
||||
}
|
||||
|
||||
// EndPoint represents and end point to send a webhook to: url and secret by which payload is signed
|
||||
// EndPoint represents a URL to send the webhook to, as well as a secret used
|
||||
// to sign the event
|
||||
type EndPoint struct {
|
||||
URL string
|
||||
Secret string
|
||||
|
@ -58,12 +63,12 @@ func Send(endPoint EndPoint, data interface{}) error {
|
|||
return senderInstance.Send(endPoint, data)
|
||||
}
|
||||
|
||||
// SendAll sends data to each of the EndPoints
|
||||
// SendAll sends data to multiple EndPoints
|
||||
func SendAll(endPoints []EndPoint, data interface{}) {
|
||||
for _, ept := range endPoints {
|
||||
go func(ept1 EndPoint) {
|
||||
senderInstance.Send(ept1, data)
|
||||
}(EndPoint{URL: ept.URL, Secret: ept.Secret})
|
||||
for _, e := range endPoints {
|
||||
go func(e EndPoint) {
|
||||
senderInstance.Send(e, data)
|
||||
}(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,7 +81,15 @@ func (ds defaultSender) Send(endPoint EndPoint, data interface{}) error {
|
|||
}
|
||||
|
||||
req, err := http.NewRequest("POST", endPoint.URL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
signat, err := sign(endPoint.Secret, jsonData)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
req.Header.Set(SignatureHeader, fmt.Sprintf("%s=%s", Sha256Prefix, signat))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := ds.client.Do(req)
|
||||
|
|
|
@ -3,6 +3,7 @@ const path = require('path');
|
|||
module.exports = {
|
||||
context: path.resolve(__dirname, 'static', 'js', 'src', 'app'),
|
||||
entry: {
|
||||
passwords: './passwords',
|
||||
users: './users',
|
||||
webhooks: './webhooks',
|
||||
},
|
||||
|
|
|
@ -125,6 +125,11 @@ func (w *DefaultWorker) LaunchCampaign(c models.Campaign) {
|
|||
// that implements an interface as a slice of that interface.
|
||||
mailEntries := []mailer.Mail{}
|
||||
currentTime := time.Now().UTC()
|
||||
campaignMailCtx, err := models.GetCampaignMailContext(c.Id, c.UserId)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
for _, m := range ms {
|
||||
// Only send the emails scheduled to be sent for the past minute to
|
||||
// respect the campaign scheduling options
|
||||
|
@ -132,7 +137,7 @@ func (w *DefaultWorker) LaunchCampaign(c models.Campaign) {
|
|||
m.Unlock()
|
||||
continue
|
||||
}
|
||||
err = m.CacheCampaign(&c)
|
||||
err = m.CacheCampaign(&campaignMailCtx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
|
@ -150,11 +155,3 @@ func (w *DefaultWorker) SendTestEmail(s *models.EmailRequest) error {
|
|||
}()
|
||||
return <-s.ErrorChan
|
||||
}
|
||||
|
||||
// errorMail is a helper to handle erroring out a slice of Mail instances
|
||||
// in the case that an unrecoverable error occurs.
|
||||
func errorMail(err error, ms []mailer.Mail) {
|
||||
for _, m := range ms {
|
||||
m.Error(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,9 +15,7 @@ type logMailer struct {
|
|||
queue chan []mailer.Mail
|
||||
}
|
||||
|
||||
func (m *logMailer) Start(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
func (m *logMailer) Start(ctx context.Context) {}
|
||||
|
||||
func (m *logMailer) Queue(ms []mailer.Mail) {
|
||||
m.queue <- ms
|
||||
|
|
Loading…
Reference in New Issue