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:
Copilot
2026-02-07 12:05:22 +02:00
committed by GitHub
parent 9ced6af6f0
commit ff6a78b8bc
10 changed files with 54 additions and 7 deletions

View File

@@ -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
View 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()
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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})
}

View File

@@ -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 {

View File

@@ -0,0 +1 @@
ALTER TABLE backend.audit_logs DROP COLUMN IF EXISTS ip_address;

View File

@@ -0,0 +1 @@
ALTER TABLE backend.audit_logs ADD COLUMN ip_address INET;

View File

@@ -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;

View File

@@ -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",