Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate audit logs on login #382

Merged
merged 54 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
0e1d356
feat: Initial migration and model updates for AuditLog improvements
juggernot325 Jan 5, 2024
d0c6173
feat: populate actor_email for existing audit_logs
juggernot325 Jan 8, 2024
1d08960
chore: Fix audit log docs to show the actual response object instead …
juggernot325 Jan 9, 2024
6019c99
feat: Log Requester IP for source and actor email with audit logs
juggernot325 Jan 11, 2024
0f0d55a
wip: createUser audit logging via middleware
juggernot325 Jan 12, 2024
3d65a85
chore: refactor audit logging to trigger from LoggingMiddleware based…
juggernot325 Jan 19, 2024
86b77a3
wip: Successfully creating audit logs from middleware. Capturing fail…
juggernot325 Jan 23, 2024
22c3e8c
feat: Capture DB errors in audit logs
juggernot325 Jan 23, 2024
cf4b854
chore: remove debug logging
juggernot325 Jan 23, 2024
985d580
chore: Refactor setting of status to receiver function
juggernot325 Jan 23, 2024
29673a9
wip: All "simple" audit context endpoints updated for middeware
juggernot325 Jan 24, 2024
a05ba51
fix: Fix a few bugs found during testing
juggernot325 Jan 24, 2024
cd78c04
fix: Remove doc for non-existent POST /api/v2/saml endpoint
juggernot325 Jan 24, 2024
9cb8a3d
wip: Refactor to simplify setting of AuditCtx in endpoints
juggernot325 Jan 25, 2024
e1d81bd
fix: registration issue after rebase
superlinkx Jan 25, 2024
3ac13c1
tasks 6 and 7 for #345 (#357)
irshadaj Jan 26, 2024
fd32507
Merge remote-tracking branch 'origin/main' into populate-audit-log-fi…
superlinkx Jan 29, 2024
2a14ff5
redo IP parsing for changed requirements (#362)
irshadaj Jan 29, 2024
602d04b
feat: audit log steel thread (#364)
superlinkx Jan 30, 2024
6fe1f94
chore: steel thread cleanup (#365)
superlinkx Jan 30, 2024
6810bd3
add eula_accepted to user audit data (#371)
irshadaj Jan 30, 2024
0482b61
audit logs for auth tokens and secrets (#372)
irshadaj Jan 31, 2024
af01314
Merge branch 'main' into populate-audit-log-fields
superlinkx Jan 31, 2024
9ff9831
Merge remote-tracking branch 'origin/main' into populate-audit-log-fi…
superlinkx Jan 31, 2024
3902523
feat: UpdateAssetGroup, UpdateAssetGroupSelector, DeleteAssetGroupSel…
superlinkx Jan 31, 2024
d744c31
more auth handlers for audit log (#373)
irshadaj Jan 31, 2024
061dc91
chore: update documentation for our new commit_id field and `intent` …
superlinkx Jan 31, 2024
4cd2264
Generate audit log entries on unauthorized access attempts (#375)
juggernot325 Jan 31, 2024
13f4160
chore: change source column to TEXT
juggernot325 Feb 1, 2024
2a2c839
rename audit log source column, add indices
irshadaj Feb 1, 2024
cc16a11
Revert "rename audit log source column, add indices"
juggernot325 Feb 1, 2024
2624969
chore: rename audit_log.source as 'source' is a reserved keyword in p…
juggernot325 Feb 1, 2024
c36c609
chore: add additional fields to user.AuditData
juggernot325 Feb 1, 2024
61427c1
feat: WIP - create audit log entries on login
juggernot325 Feb 1, 2024
d1b66da
chore: properly set the audit log status if there was an error upon l…
juggernot325 Feb 2, 2024
ac50e4f
chore: remove debug logging
juggernot325 Feb 2, 2024
15b24c5
chore: remove reference code
juggernot325 Feb 2, 2024
11cbfce
chore: capture error messages in audit logs on failure
juggernot325 Feb 2, 2024
f37e282
chore: include RequestID in login audit logs
juggernot325 Feb 2, 2024
eb5e741
Refactored unauthorized access audit logging: (#381)
sircodemane Feb 2, 2024
7bb94e7
chore: WIP - Refactor audit loggin in authenticator
juggernot325 Feb 2, 2024
0719913
chore: cleanup before review
juggernot325 Feb 2, 2024
847c92c
Merge branch 'main' into populate-audit-log-fields
juggernot325 Feb 2, 2024
a713b3f
Merge branch 'main' into audit-log-new-actions
juggernot325 Feb 5, 2024
47735bd
Merge branch 'populate-audit-log-fields' into audit-log-new-actions
juggernot325 Feb 5, 2024
8ae1344
chore: refactor login auditing to use 2-record method
juggernot325 Feb 5, 2024
0584311
chore: remove debug logging
juggernot325 Feb 5, 2024
c92a9c7
chore: update mocks
juggernot325 Feb 5, 2024
c106534
fix: move generate.go to separate package
juggernot325 Feb 5, 2024
910a342
Merge branch 'main' into audit-log-new-actions
juggernot325 Feb 5, 2024
dfd5b2c
Merge branch 'main' into audit-log-new-actions
juggernot325 Feb 5, 2024
e8fb51c
chore: address PR comments. Small refactor on parseUserIp to strip th…
juggernot325 Feb 6, 2024
be03293
Merge branch 'main' into audit-log-new-actions
juggernot325 Feb 6, 2024
047465c
fix: always populate actor fields if a user was found in the DB
juggernot325 Feb 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 64 additions & 11 deletions cmd/api/src/api/auth.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// Copyright 2023 Specter Ops, Inc.
//
//
// Licensed under the Apache License, Version 2.0
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//
// http://www.apache.org/licenses/LICENSE-2.0
//
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//
// SPDX-License-Identifier: Apache-2.0

package api
Expand All @@ -35,7 +35,9 @@ import (
"github.com/specterops/bloodhound/log"
"github.com/specterops/bloodhound/src/auth"
"github.com/specterops/bloodhound/src/config"
"github.com/specterops/bloodhound/src/ctx"
"github.com/specterops/bloodhound/src/database"
"github.com/specterops/bloodhound/src/database/types"
"github.com/specterops/bloodhound/src/model"
)

Expand Down Expand Up @@ -84,22 +86,73 @@ func NewAuthenticator(cfg config.Configuration, db database.Database, ctxInitial
}
}

func (s authenticator) LoginWithSecret(ctx context.Context, loginRequest LoginRequest) (LoginDetails, error) {
func (s authenticator) auditLogin(requestContext context.Context, commitID uuid.UUID, user model.User, loginRequest LoginRequest, status string, loginError error) {
bhCtx := ctx.Get(requestContext)
auditLog := model.AuditLog{
Action: "LoginAttempt",
Fields: types.JSONUntypedObject{"username": loginRequest.Username},
RequestID: bhCtx.RequestID,
SourceIpAddress: bhCtx.RequestIP,
Status: status,
CommitID: commitID,
}

if user.PrincipalName != "" {
auditLog.ActorID = user.ID.String()
auditLog.ActorName = user.PrincipalName
auditLog.ActorEmail = user.EmailAddress.ValueOrZero()
}

if status == string(model.AuditStatusFailure) {
auditLog.Fields["error"] = loginError
}

s.db.CreateAuditLog(auditLog)
}

func (s authenticator) validateSecretLogin(ctx context.Context, loginRequest LoginRequest) (model.User, string, error) {
if user, err := s.db.LookupUser(loginRequest.Username); err != nil {
if errors.Is(err, database.ErrNotFound) {
return LoginDetails{}, ErrInvalidAuth
return model.User{}, "", ErrInvalidAuth
}

return LoginDetails{}, FormatDatabaseError(err)
return model.User{}, "", FormatDatabaseError(err)
} else if user.AuthSecret == nil {
return LoginDetails{}, ErrNoUserSecret
return user, "", ErrNoUserSecret
} else if err := s.ValidateSecret(ctx, loginRequest.Secret, *user.AuthSecret); err != nil {
return LoginDetails{}, err
} else if err := auth.ValidateTOTPSecret(loginRequest.OTP, *user.AuthSecret); err != nil {
return LoginDetails{}, err
return user, "", err
} else if err = auth.ValidateTOTPSecret(loginRequest.OTP, *user.AuthSecret); err != nil {
return user, "", err
} else if sessionToken, err := s.CreateSession(user, *user.AuthSecret); err != nil {
return user, "", err
} else {
return user, sessionToken, nil
}
}

func (s authenticator) LoginWithSecret(ctx context.Context, loginRequest LoginRequest) (LoginDetails, error) {
var (
commitID uuid.UUID
err error
sessionToken string
user model.User
)

commitID, err = uuid.NewV4()
if err != nil {
log.Errorf("error generating commit ID for login: %s", err)
return LoginDetails{}, err
}

s.auditLogin(ctx, commitID, user, loginRequest, string(model.AuditStatusIntent), err)

user, sessionToken, err = s.validateSecretLogin(ctx, loginRequest)

if err != nil {
s.auditLogin(ctx, commitID, user, loginRequest, string(model.AuditStatusFailure), err)
return LoginDetails{}, err
} else {
s.auditLogin(ctx, commitID, user, loginRequest, string(model.AuditStatusSuccess), err)
return LoginDetails{
User: user,
SessionToken: sessionToken,
Expand Down
15 changes: 13 additions & 2 deletions cmd/api/src/api/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package middleware
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"strconv"
Expand Down Expand Up @@ -149,11 +150,21 @@ func ContextMiddleware(next http.Handler) http.Handler {
}

func parseUserIP(r *http.Request) string {
var remoteIp string

// The point of this code is to strip the port, so we don't need to save it.
if host, _, err := net.SplitHostPort(r.RemoteAddr); err != nil {
log.Warnf("Error parsing remoteAddress 's': %s", r.RemoteAddr, err)
remoteIp = r.RemoteAddr
} else {
remoteIp = host
}

if result := r.Header.Get("X-Forwarded-For"); result == "" {
log.Warnf("No data found in X-Forwarded-For header")
return r.RemoteAddr
return remoteIp
} else {
result += "," + r.RemoteAddr
result += "," + remoteIp
return result
}
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/api/src/api/middleware/middleware_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,14 @@ func TestParseUserIP_XForwardedFor_RemoteAddr(t *testing.T) {
req.Header.Set("X-Forwarded-For", strings.Join([]string{ip1, ip2, ip3}, ","))
req.RemoteAddr = "0.0.0.0:3000"

require.Equal(t, parseUserIP(req), strings.Join([]string{ip1, ip2, ip3, req.RemoteAddr}, ","))
require.Equal(t, parseUserIP(req), strings.Join([]string{ip1, ip2, ip3, "0.0.0.0"}, ","))
}

func TestParseUserIP_RemoteAddrOnly(t *testing.T) {
req, err := http.NewRequest("GET", "/teapot", nil)
require.Nil(t, err)
req.RemoteAddr = "0.0.0.0:3000"
require.Equal(t, parseUserIP(req), req.RemoteAddr)
require.Equal(t, parseUserIP(req), "0.0.0.0")
}

func TestParsePreferHeaderWait(t *testing.T) {
Expand Down
11 changes: 10 additions & 1 deletion cmd/api/src/database/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ func newAuditLog(context context.Context, entry model.AuditEntry, idResolver aut
CommitID: entry.CommitID,
}

if entry.ErrorMsg != "" {
auditLog.Fields["error"] = entry.ErrorMsg
}

authContext := bheCtx.AuthCtx
if !authContext.Authenticated() {
return auditLog, ErrAuthContextInvalid
Expand All @@ -65,10 +69,14 @@ func (s *BloodhoundDB) AppendAuditLog(ctx context.Context, entry model.AuditEntr
if auditLog, err := newAuditLog(ctx, entry, s.idResolver); err != nil && err != ErrAuthContextInvalid {
return fmt.Errorf("audit log append: %w", err)
} else {
return CheckError(s.db.Create(&auditLog))
return s.CreateAuditLog(auditLog)
}
}

func (s *BloodhoundDB) CreateAuditLog(auditLog model.AuditLog) error {
return CheckError(s.db.Create(&auditLog))
}

func (s *BloodhoundDB) ListAuditLogs(before, after time.Time, offset, limit int, order string, filter model.SQLFilter) (model.AuditLogs, int, error) {
var (
auditLogs model.AuditLogs
Expand Down Expand Up @@ -123,6 +131,7 @@ func (s *BloodhoundDB) AuditableTransaction(ctx context.Context, auditEntry mode

if err != nil {
auditEntry.Status = model.AuditStatusFailure
auditEntry.ErrorMsg = err.Error()
} else {
auditEntry.Status = model.AuditStatusSuccess
}
Expand Down
1 change: 1 addition & 0 deletions cmd/api/src/database/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ type Database interface {
RawFirst(value any) error
Wipe() error
Migrate() error
CreateAuditLog(auditLog model.AuditLog) error
AppendAuditLog(ctx context.Context, entry model.AuditEntry) error
ListAuditLogs(before, after time.Time, offset, limit int, order string, filter model.SQLFilter) (model.AuditLogs, int, error)
CreateRole(role model.Role) (model.Role, error)
Expand Down
14 changes: 14 additions & 0 deletions cmd/api/src/database/mocks/db.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

File renamed without changes.
Loading