mirror of
https://github.com/PrivateCaptcha/PrivateCaptcha.git
synced 2026-02-08 23:09:11 -06:00
Add masked IP address field to audit logs (#277)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ribtoks <505555+ribtoks@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
@@ -77,6 +78,7 @@ type AuditLogEvent struct {
|
||||
EntityID int64
|
||||
TableName string
|
||||
SessionID string
|
||||
IPAddress netip.Addr
|
||||
OldValue interface{}
|
||||
NewValue interface{}
|
||||
Timestamp time.Time
|
||||
|
||||
18
pkg/common/ipaddr.go
Normal file
18
pkg/common/ipaddr.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package common
|
||||
|
||||
import "net/netip"
|
||||
|
||||
const (
|
||||
ipv4MaskBits = 24
|
||||
ipv6MaskBits = 48
|
||||
)
|
||||
|
||||
// MaskIPAddress masks an IP address for privacy by zeroing the last octet for IPv4
|
||||
// or last 80 bits for IPv6. Returns the masked address.
|
||||
func MaskIPAddress(ip netip.Addr) netip.Addr {
|
||||
if ip.Is4() {
|
||||
return netip.PrefixFrom(ip, ipv4MaskBits).Masked().Addr()
|
||||
}
|
||||
|
||||
return netip.PrefixFrom(ip, ipv6MaskBits).Masked().Addr()
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/common"
|
||||
@@ -73,6 +74,11 @@ func (al *AuditLog) PersistAuditLog(ctx context.Context, batch []*common.AuditLo
|
||||
source = dbgen.AuditLogSourceApi
|
||||
}
|
||||
|
||||
var ipAddress *netip.Addr
|
||||
if e.IPAddress.IsValid() {
|
||||
ipAddress = &e.IPAddress
|
||||
}
|
||||
|
||||
event := &dbgen.CreateAuditLogsParams{
|
||||
UserID: Int(e.UserID),
|
||||
Action: action,
|
||||
@@ -83,6 +89,7 @@ func (al *AuditLog) PersistAuditLog(ctx context.Context, batch []*common.AuditLo
|
||||
OldValue: nil,
|
||||
NewValue: nil,
|
||||
CreatedAt: Timestampz(e.Timestamp),
|
||||
IpAddress: ipAddress,
|
||||
}
|
||||
|
||||
if e.OldValue != nil {
|
||||
@@ -150,6 +157,12 @@ func (al *AuditLog) RecordEvent(ctx context.Context, event *common.AuditLogEvent
|
||||
event.SessionID = sid
|
||||
}
|
||||
|
||||
if ip, ok := ctx.Value(common.RateLimitKeyContextKey).(netip.Addr); ok && ip.IsValid() {
|
||||
event.IPAddress = common.MaskIPAddress(ip)
|
||||
} else {
|
||||
slog.ErrorContext(ctx, "IP address not found in request context for audit log event", "table", event.TableName, "entityID", event.EntityID, "action", event.Action.String())
|
||||
}
|
||||
|
||||
event.Timestamp = time.Now().UTC()
|
||||
event.Source = source
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
@@ -21,6 +22,7 @@ type CreateAuditLogsParams struct {
|
||||
OldValue []byte `db:"old_value" json:"old_value"`
|
||||
NewValue []byte `db:"new_value" json:"new_value"`
|
||||
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||
IpAddress *netip.Addr `db:"ip_address" json:"ip_address"`
|
||||
}
|
||||
|
||||
const deleteOldAuditLogs = `-- name: DeleteOldAuditLogs :exec
|
||||
@@ -33,7 +35,7 @@ func (q *Queries) DeleteOldAuditLogs(ctx context.Context, createdAt pgtype.Times
|
||||
}
|
||||
|
||||
const getOrgAuditLogs = `-- name: GetOrgAuditLogs :many
|
||||
SELECT a.id, a.user_id, a.action, a.entity_id, a.entity_table, a.session_id, a.old_value, a.new_value, a.created_at, a.source, u.name, u.email
|
||||
SELECT a.id, a.user_id, a.action, a.entity_id, a.entity_table, a.session_id, a.old_value, a.new_value, a.created_at, a.source, a.ip_address, u.name, u.email
|
||||
FROM backend.audit_logs a
|
||||
LEFT JOIN backend.users u ON u.id = a.user_id
|
||||
WHERE (
|
||||
@@ -87,6 +89,7 @@ func (q *Queries) GetOrgAuditLogs(ctx context.Context, arg *GetOrgAuditLogsParam
|
||||
&i.AuditLog.NewValue,
|
||||
&i.AuditLog.CreatedAt,
|
||||
&i.AuditLog.Source,
|
||||
&i.AuditLog.IpAddress,
|
||||
&i.Name,
|
||||
&i.Email,
|
||||
); err != nil {
|
||||
@@ -101,7 +104,7 @@ func (q *Queries) GetOrgAuditLogs(ctx context.Context, arg *GetOrgAuditLogsParam
|
||||
}
|
||||
|
||||
const getPropertyAuditLogs = `-- name: GetPropertyAuditLogs :many
|
||||
SELECT a.id, a.user_id, a.action, a.entity_id, a.entity_table, a.session_id, a.old_value, a.new_value, a.created_at, a.source, u.name, u.email
|
||||
SELECT a.id, a.user_id, a.action, a.entity_id, a.entity_table, a.session_id, a.old_value, a.new_value, a.created_at, a.source, a.ip_address, u.name, u.email
|
||||
FROM backend.audit_logs a
|
||||
LEFT JOIN backend.users u ON u.id = a.user_id
|
||||
WHERE a.entity_table = 'properties' AND a.entity_id = $1 AND a.created_at >= $2
|
||||
@@ -148,6 +151,7 @@ func (q *Queries) GetPropertyAuditLogs(ctx context.Context, arg *GetPropertyAudi
|
||||
&i.AuditLog.NewValue,
|
||||
&i.AuditLog.CreatedAt,
|
||||
&i.AuditLog.Source,
|
||||
&i.AuditLog.IpAddress,
|
||||
&i.Name,
|
||||
&i.Email,
|
||||
); err != nil {
|
||||
@@ -162,7 +166,7 @@ func (q *Queries) GetPropertyAuditLogs(ctx context.Context, arg *GetPropertyAudi
|
||||
}
|
||||
|
||||
const getUserAuditLogs = `-- name: GetUserAuditLogs :many
|
||||
SELECT a.id, a.user_id, a.action, a.entity_id, a.entity_table, a.session_id, a.old_value, a.new_value, a.created_at, a.source, u.name, u.email
|
||||
SELECT a.id, a.user_id, a.action, a.entity_id, a.entity_table, a.session_id, a.old_value, a.new_value, a.created_at, a.source, a.ip_address, u.name, u.email
|
||||
FROM backend.audit_logs a
|
||||
LEFT JOIN backend.users u ON u.id = a.user_id
|
||||
WHERE (a.user_id = $1 OR
|
||||
@@ -222,6 +226,7 @@ func (q *Queries) GetUserAuditLogs(ctx context.Context, arg *GetUserAuditLogsPar
|
||||
&i.AuditLog.NewValue,
|
||||
&i.AuditLog.CreatedAt,
|
||||
&i.AuditLog.Source,
|
||||
&i.AuditLog.IpAddress,
|
||||
&i.Name,
|
||||
&i.Email,
|
||||
); err != nil {
|
||||
|
||||
@@ -38,6 +38,7 @@ func (r iteratorForCreateAuditLogs) Values() ([]interface{}, error) {
|
||||
r.rows[0].OldValue,
|
||||
r.rows[0].NewValue,
|
||||
r.rows[0].CreatedAt,
|
||||
r.rows[0].IpAddress,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -46,5 +47,5 @@ func (r iteratorForCreateAuditLogs) Err() error {
|
||||
}
|
||||
|
||||
func (q *Queries) CreateAuditLogs(ctx context.Context, arg []*CreateAuditLogsParams) (int64, error) {
|
||||
return q.db.CopyFrom(ctx, []string{"backend", "audit_logs"}, []string{"user_id", "action", "source", "entity_id", "entity_table", "session_id", "old_value", "new_value", "created_at"}, &iteratorForCreateAuditLogs{rows: arg})
|
||||
return q.db.CopyFrom(ctx, []string{"backend", "audit_logs"}, []string{"user_id", "action", "source", "entity_id", "entity_table", "session_id", "old_value", "new_value", "created_at", "ip_address"}, &iteratorForCreateAuditLogs{rows: arg})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package generated
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"time"
|
||||
@@ -318,6 +319,7 @@ type AuditLog struct {
|
||||
NewValue []byte `db:"new_value" json:"new_value"`
|
||||
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||
Source AuditLogSource `db:"source" json:"source"`
|
||||
IpAddress *netip.Addr `db:"ip_address" json:"ip_address"`
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE backend.audit_logs DROP COLUMN IF EXISTS ip_address;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE backend.audit_logs ADD COLUMN ip_address INET;
|
||||
@@ -1,6 +1,6 @@
|
||||
-- name: CreateAuditLogs :copyfrom
|
||||
INSERT INTO backend.audit_logs (user_id, action, source, entity_id, entity_table, session_id, old_value, new_value, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);
|
||||
INSERT INTO backend.audit_logs (user_id, action, source, entity_id, entity_table, session_id, old_value, new_value, created_at, ip_address)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
|
||||
|
||||
-- name: DeleteOldAuditLogs :exec
|
||||
DELETE FROM backend.audit_logs WHERE created_at < $1;
|
||||
|
||||
6
widget/package-lock.json
generated
6
widget/package-lock.json
generated
@@ -671,6 +671,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -939,6 +940,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -2065,7 +2067,8 @@
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"acorn-jsx": {
|
||||
"version": "5.3.2",
|
||||
@@ -2271,6 +2274,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
||||
Reference in New Issue
Block a user