132 lines
4.1 KiB
JavaScript
132 lines
4.1 KiB
JavaScript
import { getPostgresClient } from './postgres-client.js'
|
|
import { createCspAllowlistCandidates } from '../../lib/csp-policy-suggestions.js'
|
|
|
|
/**
|
|
* CSP 위반을 원본 없이 정규화된 차원별로 집계한다.
|
|
* @param {Array<{ policyVersion: string, documentPath: string, effectiveDirective: string, blockedOrigin: string, userAgentFamily: string }>} reports - CSP 집계 행
|
|
* @returns {Promise<void>}
|
|
*/
|
|
export const recordCspReports = async (reports) => {
|
|
const sql = getPostgresClient()
|
|
if (!sql || reports.length === 0) {
|
|
return
|
|
}
|
|
|
|
await sql.begin(async (transaction) => {
|
|
for (const report of reports) {
|
|
await transaction`
|
|
INSERT INTO csp_violation_aggregates (
|
|
policy_version,
|
|
document_path,
|
|
effective_directive,
|
|
blocked_origin,
|
|
user_agent_family,
|
|
violation_count,
|
|
first_seen_at,
|
|
last_seen_at
|
|
)
|
|
VALUES (
|
|
${report.policyVersion},
|
|
${report.documentPath},
|
|
${report.effectiveDirective},
|
|
${report.blockedOrigin},
|
|
${report.userAgentFamily},
|
|
1,
|
|
now(),
|
|
now()
|
|
)
|
|
ON CONFLICT (
|
|
policy_version,
|
|
document_path,
|
|
effective_directive,
|
|
blocked_origin,
|
|
user_agent_family
|
|
)
|
|
DO UPDATE SET
|
|
violation_count = csp_violation_aggregates.violation_count + 1,
|
|
last_seen_at = now()
|
|
`
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 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>, allowlistCandidates: 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: [],
|
|
allowlistCandidates: []
|
|
}
|
|
}
|
|
|
|
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 recentRows = 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 200
|
|
`
|
|
const topViolations = recentRows.slice(0, limit).map(normalizeCspViolationRow)
|
|
|
|
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,
|
|
allowlistCandidates: createCspAllowlistCandidates(
|
|
recentRows.map(normalizeCspViolationRow),
|
|
{ limit: 6 }
|
|
)
|
|
}
|
|
}
|