88 lines
2.9 KiB
JavaScript
88 lines
2.9 KiB
JavaScript
const directiveAliases = new Map([
|
|
['script-src-elem', 'script-src'],
|
|
['script-src-attr', 'script-src'],
|
|
['style-src-elem', 'style-src'],
|
|
['style-src-attr', 'style-src'],
|
|
['child-src', 'frame-src']
|
|
])
|
|
|
|
const supportedSourceDirectives = new Set([
|
|
'script-src',
|
|
'style-src',
|
|
'img-src',
|
|
'font-src',
|
|
'media-src',
|
|
'frame-src',
|
|
'connect-src',
|
|
'worker-src',
|
|
'manifest-src'
|
|
])
|
|
|
|
/**
|
|
* CSP 위반 지시어를 출처 허용 목록 후보용 지시어로 보정한다.
|
|
* @param {string} directive - CSP effective directive
|
|
* @returns {string} 출처 지시어
|
|
*/
|
|
export const normalizeCspSourceDirective = (directive) => {
|
|
const value = String(directive || '').trim().toLowerCase()
|
|
const normalized = directiveAliases.get(value) || value
|
|
return supportedSourceDirectives.has(normalized) ? normalized : ''
|
|
}
|
|
|
|
/**
|
|
* CSP 허용 후보로 검토할 수 있는 외부 origin인지 확인한다.
|
|
* @param {string} origin - 차단 origin
|
|
* @returns {boolean} 허용 후보 여부
|
|
*/
|
|
export const isCspAllowlistOriginCandidate = (origin) => {
|
|
return /^https?:\/\/[a-z0-9.-]+(?::\d+)?$/i.test(String(origin || '').trim())
|
|
}
|
|
|
|
/**
|
|
* CSP 위반 행에서 강제 정책 허용 후보를 집계한다.
|
|
* @param {Array<{ effectiveDirective?: string, blockedOrigin?: string, violationCount?: number, lastSeenAt?: string | null }>} rows - CSP 위반 행
|
|
* @param {{ limit?: number }} [options] - 후보 생성 옵션
|
|
* @returns {Array<{ sourceDirective: string, blockedOrigin: string, violationCount: number, lastSeenAt: string | null }>} 허용 후보
|
|
*/
|
|
export const createCspAllowlistCandidates = (rows, options = {}) => {
|
|
const limit = Math.min(Math.max(Number(options.limit || 6), 1), 20)
|
|
const groups = new Map()
|
|
|
|
for (const row of rows || []) {
|
|
const sourceDirective = normalizeCspSourceDirective(row.effectiveDirective)
|
|
const blockedOrigin = String(row.blockedOrigin || '').trim()
|
|
if (!sourceDirective || !isCspAllowlistOriginCandidate(blockedOrigin)) {
|
|
continue
|
|
}
|
|
|
|
const key = `${sourceDirective}\n${blockedOrigin}`
|
|
const existing = groups.get(key) || {
|
|
sourceDirective,
|
|
blockedOrigin,
|
|
violationCount: 0,
|
|
lastSeenAt: null
|
|
}
|
|
const nextCount = Number(row.violationCount || 0)
|
|
existing.violationCount += Number.isFinite(nextCount) ? Math.max(nextCount, 0) : 0
|
|
|
|
const existingLastSeen = existing.lastSeenAt ? new Date(existing.lastSeenAt).getTime() : 0
|
|
const rowLastSeen = row.lastSeenAt ? new Date(row.lastSeenAt).getTime() : 0
|
|
if (rowLastSeen > existingLastSeen) {
|
|
existing.lastSeenAt = row.lastSeenAt
|
|
}
|
|
|
|
groups.set(key, existing)
|
|
}
|
|
|
|
return [...groups.values()]
|
|
.sort((a, b) => {
|
|
const countDelta = Number(b.violationCount || 0) - Number(a.violationCount || 0)
|
|
if (countDelta !== 0) {
|
|
return countDelta
|
|
}
|
|
|
|
return new Date(b.lastSeenAt || 0).getTime() - new Date(a.lastSeenAt || 0).getTime()
|
|
})
|
|
.slice(0, limit)
|
|
}
|