CSP 관찰 요약 추가

This commit is contained in:
2026-07-29 16:07:36 +09:00
parent 23f0e9b3ef
commit c4493bb7b2
12 changed files with 245 additions and 9 deletions

View File

@@ -48,3 +48,77 @@ export const recordCspReports = async (reports) => {
}
})
}
/**
* CSP 위반 집계 행을 관리자 표시용 값으로 변환한다.
* @param {Object} row - DB 집계 행
* @returns {Object} 관리자 표시용 집계 행
*/
const normalizeCspViolationRow = (row) => {
return {
policyVersion: row.policy_version || '',
documentPath: row.document_path || '/',
effectiveDirective: row.effective_directive || 'unknown',
blockedOrigin: row.blocked_origin || 'unknown',
userAgentFamily: row.user_agent_family || 'Other',
violationCount: Number(row.violation_count || 0),
firstSeenAt: row.first_seen_at || null,
lastSeenAt: row.last_seen_at || null
}
}
/**
* 관리자 CSP 관찰용 최근 위반 집계 요약을 조회한다.
* @param {{ days?: number, limit?: number }} [options] - 조회 옵션
* @returns {Promise<{ days: number, totalViolations: number, totalRows: number, recentViolations: number, latestSeenAt: string | null, topViolations: Array<Object> }>} CSP 위반 요약
*/
export const getCspViolationSummary = async (options = {}) => {
const sql = getPostgresClient()
const days = Math.min(Math.max(Number(options.days || 30), 1), 90)
const limit = Math.min(Math.max(Number(options.limit || 8), 1), 20)
if (!sql) {
return {
days,
totalViolations: 0,
totalRows: 0,
recentViolations: 0,
latestSeenAt: null,
topViolations: []
}
}
const [summary] = await sql`
SELECT
COALESCE(SUM(violation_count), 0)::bigint AS total_violations,
COUNT(*)::bigint AS total_rows,
COALESCE(SUM(violation_count) FILTER (WHERE last_seen_at >= now() - (${days}::int * interval '1 day')), 0)::bigint AS recent_violations,
MAX(last_seen_at) AS latest_seen_at
FROM csp_violation_aggregates
`
const topRows = await sql`
SELECT
policy_version,
document_path,
effective_directive,
blocked_origin,
user_agent_family,
violation_count,
first_seen_at,
last_seen_at
FROM csp_violation_aggregates
WHERE last_seen_at >= now() - (${days}::int * interval '1 day')
ORDER BY violation_count DESC, last_seen_at DESC
LIMIT ${limit}
`
return {
days,
totalViolations: Number(summary?.total_violations || 0),
totalRows: Number(summary?.total_rows || 0),
recentViolations: Number(summary?.recent_violations || 0),
latestSeenAt: summary?.latest_seen_at || null,
topViolations: topRows.map(normalizeCspViolationRow)
}
}

View File

@@ -0,0 +1,17 @@
import { requireAdminSession } from '../../../../utils/admin-auth.js'
import { getCspViolationSummary } from '../../../../repositories/csp-report-repository.js'
/**
* 관리자 CSP 위반 관찰 요약 API
* @param {import('h3').H3Event} event - 요청 이벤트
* @returns {Promise<Object>} CSP 위반 집계 요약
*/
export default defineEventHandler(async (event) => {
requireAdminSession(event)
const query = getQuery(event)
return getCspViolationSummary({
days: Number(query.days) || 30,
limit: Number(query.limit) || 8
})
})