Devin.KR

사고 타임라인과 불확실성

65분 안팎

학습 목표

관찰 시각과 사건 시각을 구분합니다.

개념

경보를 사건 기록으로 바꿉니다

앞 모듈에서 실패 세 건 뒤 성공과 자료 거절을 연결했습니다. 그 결과는 조사할 후보이지 계정 침해 확정이 아닙니다. 대응 담당자가 처음 해야 할 일은 같은 근거를 같은 순서로 볼 수 있는 사건 기록을 만드는 것입니다. 타임라인에는 누가 언제 무엇을 관찰했는지, 어떤 파일의 어느 사건을 근거로 삼았는지 적습니다. 날짜와 문장을 정리하는 일처럼 보이지만 조사 구간과 판단 한계를 유지하는 기술 작업입니다.

이번 모듈의 실습 환경

본인 소유 로컬 환경과 합성 자료만 사용합니다. Python 3와 Node.js가 필요하며 외부 요청은 보내지 않습니다. 브라우저 과제는 JSON 배열을 표준 입력으로 받고 정해진 텍스트를 출력합니다. 로컬 과제는 내려받은 ZIP 루트에서 bash check.sh로 실행합니다. starter의 TODO를 고치고 테스트 파일은 유지합니다. 마지막 미션은 m09 solution을 이어받으며 기존 Docker 검사는 external PENDING으로 남습니다. 실제 개인정보와 자격 증명을 넣지 않습니다.

발생 시각과 관찰 시각

eventTime은 사건이 발생했다고 기록된 시각이고 observedTime은 수집기나 조사자가 그 기록을 받은 시각입니다. 지연된 로그에서는 두 시각이 다릅니다. 앞 모듈 fixture에는 time만 있으므로 eventTime으로 옮기고 observedTime은 미확인으로 남깁니다. 파일을 오늘 열었다는 이유로 사건 시각을 오늘로 채우면 과거 사건의 순서가 바뀝니다. 대응 조치의 실행 시각은 조사자가 실제 조치를 한 별도 시각으로 기록해야 합니다.

문자열 정렬의 함정

2026-10-09T09:02:00+09:00과 2026-10-09T00:02:00Z는 같은 순간입니다. 표시 문자열만 비교하면 한국 시간 사건이 더 늦어 보입니다. datetime.fromisoformat으로 값을 파싱하고 tzinfo가 있는지 확인한 뒤 astimezone으로 UTC로 변환합니다. 시간대가 없는 입력에 이 컴퓨터의 시간대를 붙이지 않습니다. 더 읽기에서는 날짜 객체의 사용법을 확장하고 여기서는 기록의 신뢰도와 비교 계약을 집중해서 다룹니다.

브라우저 과제의 계약

입력은 id와 time을 가진 객체 배열입니다. id는 비어 있지 않은 문자열이며 유효한 항목만 제공합니다. time은 초 단위 ISO 시각 문자열 또는 null입니다. 시간대 누락·파싱 불가도 MISSING으로 표시합니다. 정상 사건은 UTC 시각과 id 순으로 정렬해 시각, id를 한 줄로 출력합니다. 같은 순간이면 id 순으로 표시하지만 그 순서가 실제 인과 순서를 뜻하지 않습니다. 결측과 중복 요약은 정렬된 사건 뒤에 붙입니다.

중복을 숨기지 않습니다

같은 id가 여러 번 나타나면 첫 행을 분석에 사용하고 나머지는 DUPLICATE 요약에 그 id를 한 번 표시합니다. 이는 이 과제의 고정 정책이며 충돌한 두 행 중 첫 행이 진실이라는 주장이 아닙니다. time이 다른 중복도 원본을 보존한 채 담당자가 재전송인지 키 충돌인지 조사해야 합니다. id가 처음 나온 행에 시간이 없다면 다음 중복의 시간으로 채우지 않고 MISSING에 남깁니다. 조용히 마지막 행을 남기는 방식은 근거를 바꿀 수 있습니다.

결측 표시의 의미

시간을 정하지 못한 id는 MISSING 뒤에 사전순으로 출력합니다. 목록이 없으면 대시를 출력합니다. DUPLICATE도 같은 형식입니다. 빈 입력에서도 MISSING -와 DUPLICATE - 두 줄은 출력하여 입력 검사가 끝났다는 사실을 표시합니다. 이 출력은 로그 수집 완전성이나 사건 부재를 인증하지 않습니다. 앞 모듈의 coverage unknown은 m1이 빠졌다고 정상으로 바꾸지 않으며 최종 보고서에도 유지합니다.

오류 메시지로 범위를 좁힙니다

JSONDecodeError는 JSON 배열 구조가 깨졌는지 확인할 신호입니다. 줄 단위 객체 여러 개를 배열 대신 넣었거나 쉼표가 빠졌는지 봅니다. datetime 비교의 TypeError가 나타나면 시간대 있는 값과 없는 값을 섞었는지 확인합니다. ValueError는 이번 과제에서 잡아서 결측으로 남기므로 전체 분석이 중단되지 않습니다. broad except로 모든 오류를 숨기면 id 계약 위반이나 코드 오타까지 정상 결측처럼 보이므로 파싱 오류만 처리합니다.

관찰과 가설을 두 문장으로 나눕니다

관찰 문장은 f1부터 f3까지 인증 실패, s1 성공, d1 자료 거절이 입력 fixture에 있다고 적습니다. 가설 문장은 정상 사용자의 오타 또는 계정 침해 시도 가능성이 있어 추가 확인이 필요하다고 적습니다. access_denied는 통제가 접근을 거절했다는 근거이며 자료를 읽었다는 증거가 아닙니다. 사건 시각과 조사자가 발견한 시각이 없는 상태에서 공격이 몇 분간 지속됐다고 단정하는 문장은 쓰지 않습니다.

근거 파일을 다시 찾게 만듭니다

타임라인 각 행에 원본 파일 이름과 사건 id를 연결하면 다음 담당자가 같은 행을 재검토할 수 있습니다. 원본의 해시와 분석 규칙 버전을 함께 기록하면 분석 대상의 바이트가 바뀌었는지도 확인할 수 있습니다. 타임라인 출력 자체는 원본보다 필드가 적으므로 원본을 대체하지 않습니다. 비밀번호·세션 토큰·자료 본문은 사건 순서 판단에 필요하지 않으며 보고서용 요약에도 포함하지 않습니다.

같은 순간을 해석합니다

서로 다른 서버의 로그 시각이 같아도 실제 사건이 동시에 일어났다고 확정하기 어렵습니다. 시계 동기화 상태와 수집 지연이 추가 근거입니다. 이번 fixture는 시계 오차 범위를 모델링하지 않으므로 미확인 사항으로 적습니다. id는 안정적인 화면 정렬의 보조키입니다. 그 값이 작다는 이유로 먼저 발생했다고 말하지 않습니다. 정렬 결과와 인과 관계를 구분하면 정확한 출력이 과장된 판단으로 이어지는 일을 줄일 수 있습니다.

다음 대응으로 연결합니다

타임라인은 행동을 고르는 근거입니다. 미확인 필드는 수집 상태 확인 담당에게, 계정 사용 설명은 소유자 확인 담당에게 전달합니다. 훈련에서는 fixture 계정 a를 앱의 demo-alice로 연결하는 시나리오 매핑을 명시합니다. 같은 실제 신원이라는 증거를 새로 발견한 것으로 표현하지 않습니다. 다음 레슨은 이 기록을 보존한 뒤 해당 실습 계정만 격리하며, 조사 결론보다 먼저 피해 확대 방지와 정상 업무 영향을 함께 확인합니다.

시각 파싱과 변환 규칙은 Python datetime 공식 문서에서 확인할 수 있습니다.

따라하기

같은 순간 비교

두 표시를 파싱하여 같은 순간인지 확인합니다. 지역 시간 표시는 원본으로 보존합니다.

from datetime import datetime,timezone
a=datetime.fromisoformat('2026-10-09T09:02:00+09:00')
b=datetime.fromisoformat('2026-10-09T00:02:00+00:00')
print(a==b)
print(a.astimezone(timezone.utc).isoformat())

실행 결과

True
2026-10-09T00:02:00+00:00

결측 시간대 분리

시간대 없는 값을 현재 지역 시간으로 해석하지 않고 결측으로 표시합니다.

from datetime import datetime
t=datetime.fromisoformat('2026-10-09T00:00:00')
print('MISSING' if t.tzinfo is None else 'VALID')

실행 결과

MISSING

첫 중복 보존 정책 확인

첫 id의 결측은 후속 중복 값으로 채우지 않습니다. 이 정책은 충돌을 해결한 것이 아니라 드러낸 것입니다.

rows=[('f1',None),('f1','later'),('f2','valid')]
seen=set();duplicates=set()
for id,time in rows:
 if id in seen:duplicates.add(id);continue
 seen.add(id);print(id,time)
print('DUPLICATE',','.join(sorted(duplicates)))

실행 결과

f1 None
f2 valid
DUPLICATE f1

확인 문제

실습

id(비어 있지 않은 문자열)와 time(ISO 초 단위 문자열 또는 null)의 JSON 배열입니다. id 첫 행만 사용하고 반복 id는 DUPLICATE에 한 번 기록합니다. 유효한 오프셋 시각을 UTC 순간/id 순으로 정렬해 시각 id를 출력합니다. null·파싱 불가·시간대 없는 값은 MISSING에 id 사전순 쉼표 목록으로 표시합니다. DUPLICATE도 사전순 쉼표 목록입니다. 두 요약은 마지막에 각 한 줄이며 목록 없음은 -입니다. 출력 시각은 isoformat의 +00:00을 Z로 치환합니다.

모범 답안
import json,sys
from datetime import datetime,timezone
rows=json.load(sys.stdin)
seen=set();duplicates=set();missing=[];valid=[]
for e in rows:
 id=e['id']
 if id in seen:
  duplicates.add(id);continue
 seen.add(id)
 try:
  t=datetime.fromisoformat(e['time'].replace('Z','+00:00'))
  if t.tzinfo is None:raise ValueError('offset missing')
  valid.append((t.astimezone(timezone.utc),id))
 except (TypeError,AttributeError,ValueError):missing.append(id)
for t,id in sorted(valid):print(t.isoformat().replace('+00:00','Z'),id)
print('MISSING',','.join(sorted(missing)) or '-')
print('DUPLICATE',','.join(sorted(duplicates)) or '-')

더 읽기

면접 질문

  • 인증 실패 경보를 분석하는 순서를 설명합니다.