Skip to content

감사 시스템 실전

감사 시스템은 HTML 처리 과정에서 보안 이벤트를 기록하여, 잠재적 위험을 모니터링하고 해결하는 데 도움을 줍니다.

빠른 활성화

3 줄의 코드로 감사를 활성화하는 가장 간단한 설정:

go
cfg := html.DefaultConfig()
cfg.Audit = html.DefaultAuditConfig()
cfg.Audit.Enabled = true

p, _ := html.New(cfg)
defer p.Close()

활성화하면 보안 이벤트가 [AUDIT] JSON 형식으로 표준 오류에 출력됩니다.

감사 이벤트 유형

이벤트상수레벨트리거 조건
태그 차단blocked_tagwarning위험한 태그가 제거됨 (예: <script>)
속성 차단blocked_attrwarning위험한 속성이 제거됨 (예: onclick)
URL 차단blocked_urlwarning위험한 URL 이 차단됨
입력 위반input_violationcritical입력이 크기 제한을 초과함
깊이 위반depth_violationwarningDOM 중첩이 제한을 초과함
처리 타임아웃timeoutwarning단일 처리가 타임아웃됨
인코딩 문제encoding_issueinfo인코딩 감지 실패
경로 순회path_traversalcritical파일 경로에 ..가 포함되거나, AllowedBaseDir 모드에서 OS 핸들 해석 후 경로가 기준을 벗어나는 경우 (symlink/junction 방지)

감사 레벨

text
info < warning < critical
  • info: 정보성 이벤트 (인코딩 문제), 알림 불필요
  • warning: 주의가 필요한 이상 (타임아웃, 깊이 위반)
  • critical: 보안 위협 (입력 위반, 경로 순회)

내장 Sink 타입

LoggerAuditSink(기본값)

[AUDIT] 접두사와 함께 표준 오류로 출력:

go
// 기본적으로 stderr 로 출력
sink := html.NewLoggerAuditSink()

// 커스텀 Writer 로 출력
sink := html.NewLoggerAuditSinkWithWriter(os.Stdout)

WriterAuditSink

JSON Lines 를 io.Writer에 쓰며, 파일 영구 저장에 적합:

go
file, _ := os.Create("audit.jsonl")
defer file.Close()

sink := html.NewWriterAuditSink(file)

출력 형식 (줄당 하나의 JSON):

json
{"timestamp":"2026-04-30T10:00:00Z","event_type":"blocked_tag","level":"warning","message":"Blocked dangerous HTML tag: script","tag":"script"}

ChannelAuditSink

버퍼링된 channel 에 논블로킹으로 이벤트를 푸시하며, 소비자 goroutine 이 비동기로 처리합니다 — 외부 시스템 통합에 적합:

go
sink := html.NewChannelAuditSink(100)

// 감사 이벤트 소비
go func() {
    for entry := range sink.Channel() {
        sendToSIEM(entry)
    }
}()

// 유실된 이벤트 확인 (채널이 가득 차면 자동 폐기)
fmt.Printf("폐기됨: %d\n", sink.DroppedCount())

MultiSink

여러 Sink 로 팬아웃:

go
sink := html.NewMultiSink(
    html.NewWriterAuditSink(file),
    html.NewLoggerAuditSink(),
)

FilteredSink

조건으로 이벤트 필터링:

go
// critical 레벨 이벤트만 기록
sink := html.NewFilteredSink(
    fileSink,
    func(e html.AuditEntry) bool {
        return e.Level == html.AuditLevelCritical
    },
)

LevelFilteredSink

최소 레벨로 필터링:

go
// warning 이상만 기록
sink := html.NewLevelFilteredSink(fileSink, html.AuditLevelWarning)

감사 파이프라인 구축

시나리오 1: 파일 영구 저장

go
func newAuditPipeline() html.AuditSink {
    file, _ := os.OpenFile("audit.jsonl", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    return html.NewWriterAuditSink(file)
}

cfg := html.DefaultConfig()
cfg.Audit = html.DefaultAuditConfig()
cfg.Audit.Enabled = true
cfg.Audit.Sink = newAuditPipeline()

시나리오 2: 계층별 라우팅

critical 이벤트는 알림 시스템으로, 나머지 이벤트는 파일에 기록:

go
func newTieredPipeline() html.AuditSink {
    file, _ := os.Create("audit.jsonl")

    return html.NewMultiSink(
        // 모든 이벤트를 파일에 기록
        html.NewWriterAuditSink(file),
        // critical 이벤트만 추가로 알림 전송
        html.NewFilteredSink(
            html.NewChannelAuditSink(50),
            func(e html.AuditEntry) bool {
                return e.Level == html.AuditLevelCritical
            },
        ),
    )
}

시나리오 3: 고보안 모드

HighSecurityConfigHighSecurityAuditConfig 사용:

go
cfg := html.HighSecurityConfig()
cfg.Audit = html.HighSecurityAuditConfig()
cfg.Audit.Sink = html.NewMultiSink(
    html.NewWriterAuditSink(file),
    html.NewLoggerAuditSink(),
)

p, _ := html.New(cfg)

고보안 모드의 감사 특징:

  • 감사 자동 활성화
  • 모든 이벤트 유형 기록
  • 원시 값 포함 (IncludeRawValues = true), 포렌식 분석에 편리
  • 원시 값 최대 길이 500 자

감사 로그 조회

Processor 메서드를 통해 수집된 이벤트를 조회합니다:

go
p, _ := html.New(cfg)
defer p.Close()

// 콘텐츠 처리
p.Extract(data)

// 감사 로그 가져오기
entries := p.GetAuditLog()
for _, entry := range entries {
    fmt.Printf("[%s] %s: %s\n", entry.Level, entry.EventType, entry.Message)
}

// 로그 지우기
p.ClearAuditLog()

메모리 내 로그는 Processor 인스턴스에만 해당

GetAuditLog()는 Processor 메모리에 수집된 이벤트를 반환합니다. 영구 저장이 필요하면 Sink 를 설정하세요.

커스텀 Sink

AuditSink 인터페이스를 구현하여 감사 이벤트를 원하는 대상으로 전송:

go
type slackSink struct {
    webhook string
}

func (s *slackSink) Write(entry html.AuditEntry) {
    if entry.Level != html.AuditLevelCritical {
        return // critical 만 전송
    }
    msg := fmt.Sprintf("[AUDIT] %s: %s", entry.EventType, entry.Message)
    http.Post(s.webhook, "text/plain", strings.NewReader(msg))
}

func (s *slackSink) Close() error {
    return nil
}

다음 단계