Commitc340248unknown_key
feat(audit): log pre-receive policy rejections
feat(audit): log pre-receive policy rejections
When the push-policy gate (src/lib/push-policy.ts) rejects a push
the request never reaches the post-receive hook — meaning until now
those rejections were invisible in the audit log. Owners couldn't
see "Bob tried to push v1.0 last Tuesday and got blocked."
This commit adds a single fire-and-forget audit() call at the
rejection point in src/routes/git.ts:
action: "push.rejected"
userId: resolved pusher (null when anonymous)
repositoryId: the repo
ip / userAgent: from request headers
metadata: {
violations: [...the full violation strings...],
refs: [refs/heads/foo, refs/tags/v1.0, ...],
pusherSource: "pat" | "oauth" | "anonymous",
}
Wrapped in .catch(() => {}) so an audit-table hiccup can't promote
to a 500. The 403 response and policy semantics are unchanged.
The per-repo audit log UI (/:owner/:repo/settings/audit) renders
audit_log entries by `action` so this row appears alongside the
existing push.success / merge / branch_protection.update entries.
Suite: 1146 pass / 8 skip / 0 fail (no new tests — exercises only
DB-coupled paths; existing route auth tests cover the smoke).
https://claude.ai/code/session_0163vJChUuZqqtBBznUW6xBU1 file changed+21−0c3402481a285acef99f82487f6a26000c4eadfe9
1 changed file+21−0
Modifiedsrc/routes/git.ts+21−0View fileUnifiedSplit
@@ -18,6 +18,7 @@ import {
1818 formatPolicyError,
1919} from "../lib/push-policy";
2020import { resolvePusher } from "../lib/git-push-auth";
21import { audit } from "../lib/notify";
2122
2223const git = new Hono();
2324
@@ -98,6 +99,26 @@ git.post("/:owner/:repo.git/git-receive-pack", async (c) => {
9899 pusherUserId: pusher?.userId || null,
99100 });
100101 if (!decision.allowed) {
102 // Audit the rejection so owners can see blocked-push attempts
103 // even though the request never reached the post-receive hook.
104 // Fire-and-forget — never block the 403.
105 audit({
106 userId: pusher?.userId || null,
107 repositoryId: repoRow.id,
108 action: "push.rejected",
109 targetType: "repository",
110 targetId: repoRow.id,
111 ip:
112 c.req.header("x-forwarded-for") ||
113 c.req.header("x-real-ip") ||
114 undefined,
115 userAgent: c.req.header("user-agent") || undefined,
116 metadata: {
117 violations: decision.violations,
118 refs: refs.map((r) => r.refName),
119 pusherSource: pusher?.source || "anonymous",
120 },
121 }).catch(() => {});
101122 // Returning 403 with a plain-text body — git smart-HTTP clients
102123 // surface the body to the user (`remote: ` prefix). Existing
103124 // behaviour for repos with no policy is unchanged.
104125