Go · 7897 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 // Package audit writes structured rows into the auth_audit_log table for
4 // security-relevant events. The schema is generic — actor, action, target,
5 // meta JSON — so future sprints reuse this for permission changes,
6 // org-membership changes, admin actions, etc.
7 //
8 // Callers MUST NOT put secret material in the meta JSON. The values land
9 // unredacted in the DB; treat the table contents as confidential but
10 // never sensitive (no plaintext passwords, no TOTP secrets, no PATs).
11 package audit
12
13 import (
14 "context"
15 "encoding/json"
16 "errors"
17 "fmt"
18
19 "github.com/jackc/pgx/v5/pgtype"
20
21 usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc"
22 )
23
24 // DBTX matches sqlc's DBTX so callers can pass *pgxpool.Pool or a tx.
25 type DBTX = usersdb.DBTX
26
27 // Action is a typed action constant. Reuse these across packages so the
28 // set stays tight and grep-able.
29 type Action string
30
31 const (
32 Action2FAEnabled Action = "2fa_enabled"
33 Action2FADisabled Action = "2fa_disabled"
34 ActionRecoveryCodesIssued Action = "recovery_codes_issued"
35 ActionRecoveryCodeUsed Action = "recovery_code_used"
36 ActionRecoveryRegenerated Action = "recovery_codes_regenerated"
37 ActionAdminCleared2FA Action = "admin_cleared_2fa"
38 ActionPasswordChanged Action = "password_changed"
39 ActionPasswordReset Action = "password_reset_consumed"
40 ActionLoginSucceeded Action = "login_succeeded"
41 ActionLoginFailedThrottled Action = "login_failed_throttled"
42 ActionAccountSuspended Action = "account_suspended"
43 ActionSSHKeyAdded Action = "ssh_key_added"
44 ActionSSHKeyDeleted Action = "ssh_key_deleted"
45 ActionGPGKeyAdded Action = "gpg_key_added"
46 ActionGPGKeyDeleted Action = "gpg_key_deleted"
47 ActionPATCreated Action = "pat_created"
48 ActionPATRevoked Action = "pat_revoked"
49 ActionUsernameChanged Action = "username_changed"
50 ActionAccountDeleted Action = "account_deleted"
51 ActionAccountRestored Action = "account_restored"
52 ActionRepoCreated Action = "repo_created"
53 ActionRepoRenamed Action = "repo_renamed"
54 ActionRepoArchived Action = "repo_archived"
55 ActionRepoUnarchived Action = "repo_unarchived"
56 ActionRepoVisibilityChanged Action = "repo_visibility_changed"
57 ActionRepoSoftDeleted Action = "repo_soft_deleted"
58 ActionRepoRestored Action = "repo_restored"
59 ActionRepoHardDeleted Action = "repo_hard_deleted"
60 ActionRepoTransferRequested Action = "repo_transfer_requested"
61 ActionRepoTransferAccepted Action = "repo_transfer_accepted"
62 ActionRepoTransferDeclined Action = "repo_transfer_declined"
63 ActionRepoTransferCanceled Action = "repo_transfer_canceled"
64 ActionRepoTransferExpired Action = "repo_transfer_expired"
65 ActionIssueStateChanged Action = "issue_state_changed"
66 ActionIssueLockChanged Action = "issue_lock_changed"
67 ActionIssueCommentCreated Action = "issue_comment_created"
68 ActionPullStateChanged Action = "pull_state_changed"
69 ActionPullMerged Action = "pull_merged"
70 ActionStarCreated Action = "star_created"
71 ActionStarDeleted Action = "star_deleted"
72 ActionWatchSet Action = "watch_set"
73 ActionWatchUnset Action = "watch_unset"
74 ActionFollowCreated Action = "follow_created"
75 ActionFollowDeleted Action = "follow_deleted"
76 ActionRepoForked Action = "repo_forked"
77 ActionRepoForkSynced Action = "repo_fork_synced"
78
79 // S33 / SR2 H1 — webhook lifecycle. Pre-SR2 webhook create/update
80 // overloaded ActionRepoCreated; delete/toggle/ping/redeliver were
81 // not audited at all. Each event now has its own enum.
82 ActionWebhookCreated Action = "webhook_created"
83 ActionWebhookUpdated Action = "webhook_updated"
84 ActionWebhookDeleted Action = "webhook_deleted"
85 ActionWebhookActiveSet Action = "webhook_active_set"
86 ActionWebhookActiveUnset Action = "webhook_active_unset"
87 ActionWebhookPinged Action = "webhook_pinged"
88 ActionWebhookRedelivered Action = "webhook_redelivered"
89
90 // S41c — Actions secret/variable lifecycle. Metadata must include
91 // names only, never secret values.
92 ActionActionsSecretSet Action = "actions_secret_set"
93 ActionActionsSecretDeleted Action = "actions_secret_deleted"
94 ActionActionsVariableSet Action = "actions_variable_set"
95 ActionActionsVariableDeleted Action = "actions_variable_deleted"
96
97 // S41h — Actions run/job lifecycle. Metadata must stay structural:
98 // run/job ids, status, conclusion, workflow path/name. Never include
99 // event payloads, env, logs, permissions, tokens, or secret values.
100 ActionWorkflowRunCreated Action = "workflow_run_created"
101 ActionWorkflowRunStarted Action = "workflow_run_started"
102 ActionWorkflowRunCompleted Action = "workflow_run_completed"
103 ActionWorkflowJobCreated Action = "workflow_job_created"
104 ActionWorkflowJobStarted Action = "workflow_job_started"
105 ActionWorkflowJobCompleted Action = "workflow_job_completed"
106 ActionWorkflowJobCancelled Action = "workflow_job_cancelled"
107
108 // S34 — site admin actions. Always recorded with the real admin's
109 // id in actor_id; impersonation flows additionally carry the
110 // impersonated user's id in meta.impersonated_user_id.
111 ActionAdminSiteAdminGranted Action = "admin_site_admin_granted"
112 ActionAdminSiteAdminRevoked Action = "admin_site_admin_revoked"
113 ActionAdminUserSuspended Action = "admin_user_suspended"
114 ActionAdminUserUnsuspended Action = "admin_user_unsuspended"
115 ActionAdminUserForceDeleted Action = "admin_user_force_deleted"
116 ActionAdminUserPasswordReset Action = "admin_user_password_reset"
117 ActionAdminRepoForceArchived Action = "admin_repo_force_archived"
118 ActionAdminRepoForceUnarchived Action = "admin_repo_force_unarchived"
119 ActionAdminRepoForceDeleted Action = "admin_repo_force_deleted"
120 ActionAdminJobRetried Action = "admin_job_retried"
121 ActionAdminJobDiscarded Action = "admin_job_discarded"
122 ActionAdminImpersonateStarted Action = "admin_impersonate_started"
123 ActionAdminImpersonateStopped Action = "admin_impersonate_stopped"
124 ActionAdminImpersonateWriteOn Action = "admin_impersonate_write_on"
125 )
126
127 // Target is a typed target-type constant.
128 type Target string
129
130 const (
131 TargetUser Target = "user"
132 TargetRepo Target = "repo"
133 TargetOrg Target = "org"
134 TargetIssue Target = "issue"
135 TargetPull Target = "pull"
136 )
137
138 // Recorder writes audit rows. Bound to the sqlc queries handle.
139 type Recorder struct {
140 q *usersdb.Queries
141 }
142
143 // NewRecorder constructs a recorder.
144 func NewRecorder() *Recorder { return &Recorder{q: usersdb.New()} }
145
146 // Record inserts a single audit-log row. actorID is the user_id of the
147 // actor performing the action (use 0 for system / admin-CLI actions).
148 // targetID is the affected entity (use 0 for actions without a single
149 // concrete target).
150 //
151 // meta MUST be a JSON-serializable map; secrets and PII (tokens, raw
152 // passwords, etc.) MUST NOT appear here.
153 func (r *Recorder) Record(
154 ctx context.Context, db DBTX,
155 actorID int64, action Action, target Target, targetID int64, meta map[string]any,
156 ) error {
157 if action == "" || target == "" {
158 return errors.New("audit: action and target_type required")
159 }
160 if meta == nil {
161 meta = map[string]any{}
162 }
163 metaJSON, err := json.Marshal(meta)
164 if err != nil {
165 return fmt.Errorf("audit: marshal meta: %w", err)
166 }
167 var actorParam pgtype.Int8
168 if actorID != 0 {
169 actorParam = pgtype.Int8{Int64: actorID, Valid: true}
170 }
171 var targetParam pgtype.Int8
172 if targetID != 0 {
173 targetParam = pgtype.Int8{Int64: targetID, Valid: true}
174 }
175 return r.q.InsertAuditLog(ctx, db, usersdb.InsertAuditLogParams{
176 ActorID: actorParam,
177 Action: string(action),
178 TargetType: string(target),
179 TargetID: targetParam,
180 Meta: metaJSON,
181 })
182 }
183