경보 근거와 오탐 검토
90분 안팎
학습 목표
임계값과 누락·오탐 가능성을 함께 평가합니다.
개념
경보는 질문을 구체화합니다
실패 세 번 뒤 성공과 자료 거절이 연결되면 무엇을 조사해야 하는지 더 분명해집니다. 그러나 경보가 곧 침해 확정은 아닙니다. 이 레슨은 규칙을 충족한 사건을 candidate와 review로 구분하고 근거 id를 출력합니다. review는 추가 검토가 필요한 상태이며 계정 정지나 사고 확정의 자동 결정이 아닙니다. 정상 설명과 악성 설명을 함께 남겨 다음 조사자가 근거를 검토할 수 있도록 합니다.
분류 규칙을 글로 고정합니다
합성 사건 목록에서 auth_failure가 세 건 미만이면 none입니다. 세 건 이상이면 candidate입니다. 마지막 실패 이후 성공과 그 성공 뒤 같은 계정·requestId의 접근 거절이 있으면 review입니다. 입력 한 묶음은 같은 계정의 조사 구간이며 잘못된 순서는 시각으로 정렬합니다. 근거 배열에는 실패 id, 선택된 성공 id, 거절 id를 순서대로 담습니다. 정책 이름과 임계값을 결과 해설에 함께 씁니다.
오타를 설명으로 다룹니다
사용자가 비밀번호를 잊고 세 번 틀린 뒤 정상 로그인할 수 있습니다. 실패가 적으면 규칙이 발동하지 않지만 오타라는 라벨 하나로 실패를 모두 없애지 않습니다. 계정 소유자의 확인, 정상 활동과 시간, 거절 이유를 추가 근거로 검토합니다. 오탐 후보는 정상일 가능성이 있다는 뜻이며 정상임이 입증됐다는 뜻은 아닙니다. 가정과 확인된 설명을 구분하면 조사 재개 조건을 남기기 쉽습니다.
공유 주소의 함정
회사나 가정의 여러 기기는 하나의 외부 주소를 사용할 수 있습니다. 같은 IP에서 실패와 성공이 나와도 같은 사람이 했다는 증거가 되지 않습니다. 반대로 주소가 바뀌었다고 다른 사람이라고 단정할 수도 없습니다. 이번 분류는 계정과 서버 흐름 키를 사용하며 IP를 합산 조건에 넣지 않습니다. 실제 주소 정보를 보관한다면 분석 필요성과 개인정보 보관 정책을 함께 검토합니다.
임계값의 손익을 검토합니다
임계값을 낮추면 조사할 후보가 많아지고 정상 오타가 더 자주 포함될 수 있습니다. 높이면 짧은 의심 시도를 놓칠 수 있습니다. 정답 숫자 하나를 찾는 대신 고정된 라벨 fixture에서 어떤 후보가 추가되거나 빠지는지 비교합니다. 운영 적용 전에는 정상 패턴과 조사 인력, 계정 중요도를 검토해야 합니다. 이 교재의 세 건은 작은 경계 테스트를 만들기 위한 기준입니다.
근거 id가 필요한 이유
계정 이름과 위험 상태만 전달하면 다른 담당자가 어떤 사건을 봤는지 찾기 어렵습니다. 근거 id는 원본으로 돌아갈 경로이며 판정에 사용하지 않은 행과 구분됩니다. 원본 파일 해시와 조사 구간을 함께 보존하면 동일 결과를 다시 계산할 수 있습니다. 비밀번호나 자료 내용은 경보 근거에 필요하지 않습니다. 민감 원문 대신 사건 유형·시각·허용된 키를 사용해 검토 가능한 기록을 만듭니다.
결측은 관찰 공백입니다
미션은 time·account·type·requestId·id 중 하나라도 비어 있으면 missing으로 분리하고 coverage를 unknown으로 둡니다. 이를 실패 0건이나 정상으로 처리하면 수집 장애 시 경보가 사라지는 문제가 생깁니다. unknown은 사건이 악성이라는 뜻도 아닙니다. 현재 입력으로 전체 구간을 평가할 수 없음을 나타냅니다. 수집 상태 확인 담당과 재수집 조건을 기록하고 이후 확인된 데이터를 새 분석 묶음으로 보존합니다.
완전성 표기의 한계
fixture_complete는 제공된 배열의 필드 검사를 마쳤다는 뜻입니다. 실제 로그가 빠짐없이 수집됐다는 인증이 아닙니다. 수집기 heartbeat, 파일 회전 누락, 이벤트 수 변화, 서버 시간 동기화 상태는 별도 관찰이 필요합니다. 빈 배열에서도 입력 검사는 끝나지만 아무 사건이 없었다는 운영 주장은 할 수 없습니다. 보고서에서는 교재 fixture의 완전성과 현실 수집 범위를 혼동하지 않습니다.
시간대와 시간창을 보고합니다
미션은 시작 포함·끝 제외의 고정 5분 구간을 사용하고 오프셋을 UTC로 맞춥니다. 끝 시각의 실패는 현재 경보의 횟수에서 빠집니다. 다른 오프셋의 같은 순간은 같은 위치에 들어갑니다. 시간이 없는 사건에 현재 시각을 채우면 실제 순서가 바뀌므로 결측으로 남깁니다. 수집 시각과 발생 시각은 별도 필드로 관리하는 것이 유용하지만 이번 fixture에는 발생 시각만 있습니다.
판정을 과장하는 결함을 고칩니다
starter 미션은 연결된 거절을 confirmed로 분류합니다. 실패 메시지에서 expected review와 actual confirmed를 찾아 규칙 문서와 비교합니다. 거절은 접근 통제가 작동한 관찰이므로 침해 확정 근거가 아닙니다. events.cjs의 허용 목록 TODO도 함께 고쳐야 합니다. 둘 중 하나만 고치면 전체 check.sh가 계속 실패합니다. inherited 검사의 실패라면 이번 탐지 코드와 분리해서 원인을 읽습니다.
조사 기록을 인계합니다
detection-policy.md와 detection-result.json을 이전 보고서와 함께 전달합니다. 기존 manifest는 이전 증거를 봉인한 기록이므로 새 탐지 파일을 넣기 위해 조용히 덮어쓰지 않습니다. 새 산출물의 출처와 정책을 추가 묶음으로 명시합니다. 로컬 테스트가 통과해도 legacy Docker 검사는 external PENDING입니다. 다음 담당자는 추가 확인의 책임과 범위를 보고 가상 사고 타임라인을 작성할 수 있습니다.
경보 품질을 사람이 읽습니다
자동 검사는 빈 입력, 임계값 경계, 시간창 끝, 시간대 변환, 다른 계정과 흐름 키, 중복 id와 결측을 확인합니다. 정상 설명을 조사하는 대화나 실제 사용자 소유자 확인은 구현하지 않습니다. 더 읽기의 로그 분석 장에서 검색과 요약 방법을 확장합니다. 최종 제출에는 관찰한 id와 가능한 정상 설명, 아직 모르는 사항, 다음 확인 행동을 함께 적어 경보 상태가 행동으로 이어지게 합니다.
경보를 닫을 때는 상태를 바꾼 이유와 확인한 사람, 사용한 추가 근거를 남깁니다. 다음날 같은 계정의 새 사건이 생겼다면 이전 정상 설명을 새 사건에 자동 적용하지 않습니다. 규칙 버전이 바뀌면 이전 경보의 의미가 달라질 수 있어 그 버전도 보존합니다. 무해한 경우를 억제하는 예외 규칙에는 종료 조건과 재검토 기준을 적고 조사 공백이 늘지 않는지 점검합니다. 이번 fixture는 이러한 운영 승인 절차를 실행하지 않습니다.
따라하기
작은 조건을 직접 실행합니다
failures=3
denied=True
print('review' if failures>=3 and denied else 'candidate')실행 결과
review
경보 상태를 만듭니다
한 계정 UTC 입력을 정렬하고 none·candidate·review와 근거 배열을 계산합니다.
정상 설명을 기록합니다
오타·공유 주소 설명을 확정과 구분해 추가 확인 방법을 한 문장씩 적습니다.
누적 미션을 검사합니다
미션 ZIP에서 events.cjs와 detection.py TODO를 고친 뒤 bash check.sh를 실행합니다. detection-result.json과 detection-policy.md를 이전 보고서와 함께 검토합니다.
확인 문제
실습
한 계정의 유효한 사건 배열이며 time은 UTC Z의 고정 길이 문자열입니다. 실패 3건 미만 none, 3건 이상 candidate, 마지막 실패 뒤 성공과 같은 키의 후속 거절이면 review입니다. 근거는 정렬된 실패 id 뒤 첫 후속 성공 id와 거절 id입니다. JSON 객체를 sort_keys=True로 출력합니다. none의 근거는 빈 배열입니다.
모범 답안
import json,sys
from datetime import datetime
rows=json.load(sys.stdin)
rows.sort(key=lambda e:(datetime.fromisoformat(e['time'].replace('Z','+00:00')),e['id']))
f=[e for e in rows if e['type']=='auth_failure']
s=next((e for e in rows if e['type']=='auth_success' and f and e['time']>f[-1]['time']),None)
d=[] if s is None else [e for e in rows if e['type']=='access_denied' and e['account']==s['account'] and e['requestId']==s['requestId'] and e['time']>s['time']]
status='none' if len(f)<3 else 'review' if d else 'candidate'
ids=[] if status=='none' else [e['id'] for e in f]+([] if s is None else [s['id']])+[e['id'] for e in d]
print(json.dumps({'status':status,'evidence':ids},sort_keys=True))
더 읽기
면접 질문
- 인증 실패 경보를 분석하는 순서를 설명합니다.