๐Ÿ ๋ณด์•ˆ ๊ถŒ๊ณ ๋ฌธ ๋ชจ๋‹ˆํ„ฐ๋ง ์ž๋™ํ™” โ€” ์˜์กด์„ฑ 0์œผ๋กœ ๋งŒ๋“œ๋Š” RSS ์ˆ˜์ง‘๊ธฐ

@leekh8 ยท July 02, 2026 ยท 8 min read

Python ๋ณด์•ˆ ์ž๋™ํ™” ์‹œ๋ฆฌ์ฆˆ

  1. IPยท๋„๋ฉ”์ธ ํ‰ํŒ ์กฐํšŒ ์ž๋™ํ™” โ€” VirusTotal API ์‹ค์ „ ํ™œ์šฉ
  2. ๋ณด์•ˆ ๊ถŒ๊ณ ๋ฌธ ๋ชจ๋‹ˆํ„ฐ๋ง ์ž๋™ํ™” โ€” ์˜์กด์„ฑ 0์œผ๋กœ ๋งŒ๋“œ๋Š” RSS ์ˆ˜์ง‘๊ธฐ โ† ํ˜„์žฌ ๊ธ€

๋ณด์•ˆ ์—…๋ฌด์˜ ํ•˜๋ฃจ๋Š” ๋Œ€๊ฐœ "๋ฐค์‚ฌ์ด ์ƒˆ๋กœ ๋‚˜์˜จ ๊ถŒ๊ณ ๋ฌธ ํ™•์ธ"์œผ๋กœ ์‹œ์ž‘ํ•œ๋‹ค. KISA ๋ณดํ˜ธ๋‚˜๋ผ ๋ณด์•ˆ๊ณต์ง€, ์‚ฌ์šฉํ•˜๋Š” ์žฅ๋น„ ๋ฒค๋”์˜ PSIRT(Product Security Incident Response Team) ๊ณต์ง€๋ฅผ ๋ธŒ๋ผ์šฐ์ €๋กœ ํ•˜๋‚˜์”ฉ ์—ด์–ด๋ณด๋Š” ์ผ์ด ๋งค์ผ ๋ฐ˜๋ณต๋œ๋‹ค.

๋ฌธ์ œ๋Š” ์ด ์ž‘์—…์ด ํ•˜๋ฃจ๋„ ๋น ์ง€๋ฉด ์•ˆ ๋˜๋Š”๋ฐ, ์‚ฌ๋žŒ์ด ํ•˜๋ฉด ๋ฐ˜๋“œ์‹œ ๋น ์ง„๋‹ค๋Š” ์ ์ด๋‹ค. ํœด๊ฐ€, ๋ฐ”์œ ์•„์นจ, ๋‹จ์ˆœ ๊นœ๋นก์ž„ โ€” ์ด์œ ๋Š” ๋งŽ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋†“์นœ ๊ถŒ๊ณ ๋ฌธ ํ•˜๋‚˜๊ฐ€ ํŒจ์น˜ ์ง€์—ฐ์œผ๋กœ ์ด์–ด์ง„๋‹ค.

์ด ๊ธ€์—์„œ๋Š” ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒ์œผ๋กœ(์˜์กด์„ฑ 0) ๋ณด์•ˆ ๊ถŒ๊ณ ๋ฌธ RSS๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ , ์ด๋ฏธ ๋ณธ ๊ฒƒ์€ ๊ฑธ๋Ÿฌ์„œ ์ƒˆ ๊ถŒ๊ณ ๋ฌธ๋งŒ ์•Œ๋ ค์ฃผ๋Š” ์ˆ˜์ง‘๊ธฐ๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋งŒ๋“ค์–ด๋ณธ๋‹ค. ์ฝ”๋“œ ์ž์ฒด๋ณด๋‹ค ๊ฐ’์ง„ ๊ฑด ์šด์˜์—์„œ ๊ฒช๋Š” ํ•จ์ •๋“ค์ด๋‹ค โ€” ์ฒซ ์‹คํ–‰ ์•Œ๋ฆผ ํญํƒ„, ์žฌ์‹œ๋„ ์—†๋Š” fetch์˜ ์˜คํƒ, RSS ๋…ธ์ถœ ๊ฐœ์ˆ˜ ํ•œ๊ณ„ ๊ฐ™์€ ๊ฒƒ๋“ค์„ ํ•จ๊ป˜ ๋‹ค๋ฃฌ๋‹ค.


์™œ ์Šคํฌ๋ ˆ์ดํ•‘์ด ์•„๋‹ˆ๋ผ RSS์ธ๊ฐ€

๊ถŒ๊ณ ๋ฌธ ํŽ˜์ด์ง€๋ฅผ HTML ์Šคํฌ๋ ˆ์ดํ•‘์œผ๋กœ ๊ธ์„ ์ˆ˜๋„ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ณต์‹ RSS๊ฐ€ ์žˆ๋‹ค๋ฉด RSS๊ฐ€ ํ•ญ์ƒ ๋จผ์ €๋‹ค.

๋น„๊ต HTML ์Šคํฌ๋ ˆ์ดํ•‘ RSS
์•ˆ์ •์„ฑ ํŽ˜์ด์ง€ ๊ฐœํŽธ ๋•Œ๋งˆ๋‹ค ํŒŒ์„œ ๊นจ์ง RSS 2.0 ํ‘œ์ค€ ๊ตฌ์กฐ ๊ณ ์ •1
ํŒŒ์‹ฑ ๋‚œ์ด๋„ DOM ๊ตฌ์กฐ ๋ถ„์„ ํ•„์š” <item> ๋ฐ˜๋ณต๋งŒ ์ฝ์œผ๋ฉด ๋
์„œ๋ฒ„ ๋ถ€๋‹ด ํŽ˜์ด์ง€ ์ „์ฒด ๋กœ๋“œ ๊ฒฝ๋Ÿ‰ XML ํ•˜๋‚˜
๋ฒ•์ /๋งค๋„ˆ robots.txtยท์ด์šฉ์•ฝ๊ด€ ํ™•์ธ ํ•„์š” ๊ตฌ๋…ํ•˜๋ผ๊ณ  ๊ณต๊ฐœํ•œ ์ฑ„๋„

๋ณด์•ˆ ๊ถŒ๊ณ ๋ฌธ์€ ๋‹คํ–‰ํžˆ ์ฃผ์š” ๊ธฐ๊ด€ยท๋ฒค๋”๊ฐ€ RSS๋ฅผ ๊ณต์‹ ์ œ๊ณตํ•œ๋‹ค. ์ด ๊ธ€์—์„œ ์“ฐ๋Š” ๋‘ ์†Œ์Šค:

์†Œ์Šค RSS ํ•ญ๋ชฉ ๊ณ ์œ  ID
KISA ๋ณดํ˜ธ๋‚˜๋ผ ๋ณด์•ˆ๊ณต์ง€2 https://www.boho.or.kr/kr/rss.do?bbsId=B0000133 ๋งํฌ์˜ nttId ํŒŒ๋ผ๋ฏธํ„ฐ
Fortinet PSIRT (IR Advisories)3 https://filestore.fortinet.com/fortiguard/rss/ir.xml ๋งํฌ ๋์˜ FG-IR-YY-NNN

์‚ฌ์šฉํ•˜๋Š” ๋ฒค๋”๊ฐ€ ๋‹ค๋ฅด๋‹ค๋ฉด ํ•ด๋‹น ๋ฒค๋”์˜ PSIRT/Security Advisory ํŽ˜์ด์ง€์—์„œ RSS ๋งํฌ๋ฅผ ์ฐพ์œผ๋ฉด ๋œ๋‹ค. Cisco, Palo Alto, Juniper ๋“ฑ ๋Œ€๋ถ€๋ถ„์˜ ๋„คํŠธ์›Œํฌ ์žฅ๋น„ ๋ฒค๋”๊ฐ€ ์ œ๊ณตํ•œ๋‹ค.


์ „์ฒด ์„ค๊ณ„ โ€” ํŒŒ์ดํ”„๋ผ์ธ ํ•œ ์žฅ

IntegrityError
์„ฑ๊ณต
๋ฏธ์„ค์ • ์‹œ
RSS fetch

urllib + ์žฌ์‹œ๋„
ํŒŒ์‹ฑ

xml.etree
SQLite INSERT

UNIQUE ์ œ์•ฝ
์ด๋ฏธ ๋ณธ ํ•ญ๋ชฉ

์กฐ์šฉํžˆ ์Šคํ‚ต
์‹ ๊ทœ ๊ถŒ๊ณ ๋ฌธ
๋ฉ”์ผ ์•Œ๋ฆผ

smtplib
์ฝ˜์†” ์ถœ๋ ฅ

ํ•ต์‹ฌ ์„ค๊ณ„ ๊ฒฐ์ •์€ ์„ธ ๊ฐ€์ง€๋‹ค.

โ‘  ์˜์กด์„ฑ 0. urllib(HTTP), xml.etree(RSS ํŒŒ์‹ฑ), sqlite3(์ƒํƒœ ์ €์žฅ), smtplib(๋ฉ”์ผ) โ€” ์ „๋ถ€ ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‹ค. pip install ์—†์ด Python๋งŒ ์žˆ์œผ๋ฉด ์–ด๋””์„œ๋“  ๋ˆ๋‹ค. ์„œ๋ฒ„, ์‚ฌ๋‚ด๋ง, ์ƒˆ ๋…ธํŠธ๋ถ ์–ด๋””์— ์˜ฎ๊ฒจ๋„ "ํ™˜๊ฒฝ ์„ธํŒ…"์ด๋ผ๋Š” ๋‹จ๊ณ„ ์ž์ฒด๊ฐ€ ์—†๋‹ค.

โ‘ก "์ด๋ฏธ ๋ดค๋Š”์ง€"๋Š” DB UNIQUE ์ œ์•ฝ์ด ํŒ๋‹จํ•œ๋‹ค. "์ง€๋‚œ๋ฒˆ ๊ฒฐ๊ณผ์™€ ๋น„๊ต" ๋กœ์ง์„ ์ง์ ‘ ์งœ๋Š” ๋Œ€์‹ , UNIQUE(source, entry_id) ์ œ์•ฝ์„ ๊ฑธ๊ณ  INSERT๋ฅผ ์‹œ๋„ํ•œ๋‹ค. ์ด๋ฏธ ์žˆ์œผ๋ฉด IntegrityError๊ฐ€ ๋‚˜๊ณ , ๊ทธ๊ฒŒ ๊ณง "๋ณธ ์  ์žˆ์Œ"์ด๋‹ค. ๋น„๊ต ์ฝ”๋“œ๊ฐ€ 0์ค„์ด ๋œ๋‹ค.

โ‘ข ์ฒซ ์‹คํ–‰์€ seed ๋ชจ๋“œ. ์ฒ˜์Œ ๋Œ๋ฆฌ๋ฉด ํ”ผ๋“œ์˜ ๊ธฐ์กด ํ•ญ๋ชฉ ์ˆ˜์‹ญ ๊ฑด์ด ์ „๋ถ€ "์‹ ๊ทœ"๋กœ ์žกํžŒ๋‹ค. ์ด๊ฑธ ๊ทธ๋Œ€๋กœ ๋ฉ”์ผ๋กœ ์˜๋ฉด ์ฒซ๋‚ ๋ถ€ํ„ฐ ์•Œ๋ฆผ ํญํƒ„์ด๋‹ค. DB๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด(=์ฒซ ์‹คํ–‰) ์•Œ๋ฆผ ์—†์ด ์ ์žฌ๋งŒ ํ•˜๊ณ  ๋๋‚ธ๋‹ค.


๊ตฌํ˜„ 1 โ€” fetch: ์žฌ์‹œ๋„ ์—†๋Š” ์ˆ˜์ง‘๊ธฐ๋Š” ์˜คํƒ ์ œ์กฐ๊ธฐ

๊ฐ€์žฅ ํ”ํ•œ ์‹ค์ˆ˜๋ถ€ํ„ฐ ๋ณด์ž.

# โŒ ์ทจ์•ฝํ•œ ์ฝ”๋“œ โ€” ์ผ์‹œ ์žฅ์•  ํ•œ ๋ฒˆ์— "์‹ ๊ทœ 0๊ฑด"์œผ๋กœ ์กฐ์šฉํžˆ ์‹คํŒจ
def fetch(url):
    return urllib.request.urlopen(url, timeout=20).read().decode()

RSS ์„œ๋ฒ„๋„ ๊ฐ€๋” ์ฃฝ๋Š”๋‹ค. ์ผ์‹œ์ ์ธ 500 ์‘๋‹ต์ด๋‚˜ ํƒ€์ž„์•„์›ƒ ํ•œ ๋ฒˆ์— ๊ทธ๋‚  ์ˆ˜์ง‘์ด ํ†ต์งธ๋กœ ๋น ์ง€๊ณ , ์‹ฌํ•˜๋ฉด "์˜ค๋Š˜์€ ์ƒˆ ๊ถŒ๊ณ ๋ฌธ์ด ์—†๋„ค"๋ผ๋Š” ์กฐ์šฉํ•œ ๊ฑฐ์ง“ ๊ฒฐ๋ก ์ด ๋œ๋‹ค. ์˜ค๋ฅ˜์˜ ์ข…๋ฅ˜์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ๋ฐ˜์‘ํ•ด์•ผ ํ•œ๋‹ค.

์‘๋‹ต ์˜๋ฏธ ๋Œ€์‘
404 ํ”ผ๋“œ ์œ„์น˜๊ฐ€ ๋ฐ”๋€œ ์žฌ์‹œ๋„ ๋ฌด์˜๋ฏธ โ€” ์„ค์ • ์ ๊ฒ€ ์‹ ํ˜ธ๋กœ ํ‘œ๋ฉดํ™”
๊ทธ ์™ธ 4xx ์š”์ฒญ ์ž์ฒด๊ฐ€ ์ž˜๋ชป๋จ ์ฆ‰์‹œ ์˜ˆ์™ธ โ€” ์ฝ”๋“œ/์„ค์ • ๋ฒ„๊ทธ
5xx ยท ํƒ€์ž„์•„์›ƒ ยท ์—ฐ๊ฒฐ ์˜ค๋ฅ˜ ์„œ๋ฒ„/๋„คํŠธ์›Œํฌ ์ผ์‹œ ์žฅ์•  ์žฌ์‹œ๋„ (๋ฐฑ์˜คํ”„)
# โœ… ์•ˆ์ „ํ•œ ์ฝ”๋“œ โ€” ์˜ค๋ฅ˜๋ฅผ ๋ถ„๋ฅ˜ํ•ด์„œ ์žฌ์‹œ๋„ํ•  ๊ฒƒ๋งŒ ์žฌ์‹œ๋„
HTTP_TIMEOUT, HTTP_RETRIES, HTTP_BACKOFF = 20, 3, 5

def fetch(url: str) -> str | None:
    last = None
    for attempt in range(1, HTTP_RETRIES + 1):
        try:
            req = urllib.request.Request(url, headers={"User-Agent": UA})
            with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT) as res:
                return res.read().decode("utf-8", errors="replace")
        except urllib.error.HTTPError as e:
            if e.code == 404:
                return None            # ํ”ผ๋“œ ์ด๋™ โ€” ํ˜ธ์ถœ๋ถ€์—์„œ ํ‘œ๋ฉดํ™”
            if not (500 <= e.code < 600):
                raise                  # 4xx๋Š” ์ฆ‰์‹œ ๋“œ๋Ÿฌ๋‚ธ๋‹ค
            last = e
        except (urllib.error.URLError, TimeoutError, OSError) as e:
            last = e                   # ๋„คํŠธ์›Œํฌ์„ฑ โ†’ ์žฌ์‹œ๋„
        if attempt < HTTP_RETRIES:
            time.sleep(HTTP_BACKOFF * attempt)   # 5s, 10s ์„ ํ˜• ๋ฐฑ์˜คํ”„
    raise last

โš ๏ธ User-Agent๋ฅผ ๊ผญ ๋„ฃ์ž. ์ผ๋ถ€ ๊ณต๊ณต๊ธฐ๊ด€ ์„œ๋ฒ„๋Š” ๊ธฐ๋ณธ Python-urllib UA๋ฅผ ์ฐจ๋‹จํ•œ๋‹ค. ์‹๋ณ„ ๊ฐ€๋Šฅํ•œ UA(๋„๊ตฌ๋ช… ๋“ฑ)๋ฅผ ๋ช…์‹œํ•˜๋Š” ๊ฒƒ์ด ์ˆ˜์‹  ์ธก์— ๋Œ€ํ•œ ๋งค๋„ˆ์ด๊ธฐ๋„ ํ•˜๋‹ค.


๊ตฌํ˜„ 2 โ€” ํŒŒ์‹ฑ: RSS 2.0์€ xml.etree๋กœ ์ถฉ๋ถ„ํ•˜๋‹ค

feedparser ๊ฐ™์€ ์ข‹์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋„ ์žˆ์ง€๋งŒ, RSS 2.0์˜ <item> ๊ตฌ์กฐ๋Š” ํ‘œ์ค€ xml.etree๋กœ ๋ช‡ ์ค„์ด๋ฉด ๋๋‚œ๋‹ค. ์˜์กด์„ฑ 0 ์›์น™์„ ์ง€ํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.

import xml.etree.ElementTree as ET

def parse_rss(xml_text: str) -> list[dict]:
    channel = ET.fromstring(xml_text).find("channel")
    if channel is None:
        return []
    entries = []
    for item in channel.findall("item"):
        entries.append({
            "title": (item.findtext("title") or "").strip(),
            "link": (item.findtext("link") or "").strip(),
            "pubDate": (item.findtext("pubDate") or "").strip(),
            "description": (item.findtext("description") or "").strip(),
        })
    return entries

.strip()์„ ๋นผ๋†“์ง€ ๋ง์ž. ์‹ค์ œ ํ”ผ๋“œ์—๋Š” <link> https://... </link>์ฒ˜๋Ÿผ ์•ž๋’ค ๊ณต๋ฐฑ์ด ์„ž์—ฌ ์˜ค๋Š” ๊ฒฝ์šฐ๊ฐ€ ํ”ํ•˜๊ณ , ์ด ๊ณต๋ฐฑ์ด ๊ทธ๋Œ€๋กœ DB์— ๋“ค์–ด๊ฐ€๋ฉด ๊ฐ™์€ ํ•ญ๋ชฉ์ด ๋‹ค๋ฅธ ํ•ญ๋ชฉ์œผ๋กœ ์ค‘๋ณต ์ ์žฌ๋œ๋‹ค.

์†Œ์Šค๋งˆ๋‹ค "ํ•ญ๋ชฉ์˜ ๊ณ ์œ  ID"๋ฅผ ๋ฝ‘๋Š” ๋ฐฉ๋ฒ•์ด ๋‹ค๋ฅด๋‹ค๋Š” ์ ์ด ์œ ์ผํ•œ ์†Œ์Šค๋ณ„ ์ฐจ์ด๋‹ค. ์ด ์ฐจ์ด๋ฅผ ์†Œ์Šค ์ •์˜์— ๋žŒ๋‹ค๋กœ ๋„ฃ์–ด๋‘๋ฉด, ์ƒˆ ์†Œ์Šค ์ถ”๊ฐ€๊ฐ€ dict ํ•ญ๋ชฉ ํ•˜๋‚˜๋กœ ๋๋‚œ๋‹ค:

SOURCES = {
    "KISA": {
        "rss": "https://www.boho.or.kr/kr/rss.do?bbsId=B0000133",
        # ๊ฒŒ์‹œ๊ธ€ ๋งํฌ์˜ nttId ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๊ณ ์œ  ID
        "entry_id": lambda item: _query_param(item["link"], "nttId"),
    },
    "FORTINET": {
        "rss": "https://filestore.fortinet.com/fortiguard/rss/ir.xml",
        # ๋งํฌ ๋ FG-IR-YY-NNN์ด ๊ณ ์œ  ID
        "entry_id": lambda item: item["link"].rstrip("/").rsplit("/", 1)[-1],
    },
}

๊ตฌํ˜„ 3 โ€” ์ค‘๋ณต ์ œ๊ฑฐ: ๋น„๊ต ์ฝ”๋“œ ๋Œ€์‹  UNIQUE ์ œ์•ฝ

CREATE TABLE IF NOT EXISTS advisories (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    source      TEXT NOT NULL,
    entry_id    TEXT NOT NULL,
    title       TEXT NOT NULL,
    url         TEXT NOT NULL,
    published   TEXT,
    cves        TEXT,
    detected_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
    notified    INTEGER NOT NULL DEFAULT 0,
    UNIQUE(source, entry_id)          -- ์ด ํ•œ ์ค„์ด "์ด๋ฏธ ๋ดค๋Š”์ง€" ํŒ๋‹จ ์ „๋ถ€
);

์ˆ˜์ง‘ ๋ฃจํ”„๋Š” INSERT๋ฅผ ์‹œ๋„ํ•˜๊ณ , IntegrityError๋ฉด "๋ณธ ์  ์žˆ์Œ"์œผ๋กœ ๋„˜์–ด๊ฐ„๋‹ค:

for entry in parse_rss(xml_text):
    entry_id = src["entry_id"](entry)
    try:
        conn.execute(
            "INSERT INTO advisories (source, entry_id, title, url, published, cves)"
            " VALUES (?,?,?,?,?,?)",
            (key, entry_id, entry["title"], entry["link"],
             entry["pubDate"], extract_cves(entry)))
        new_count += 1
    except sqlite3.IntegrityError:
        pass  # ์ด๋ฏธ ๋ณธ ํ•ญ๋ชฉ โ€” ์กฐ์šฉํžˆ ์Šคํ‚ต

๊ถŒ๊ณ ๋ฌธ์—์„œ CVE ๋ฒˆํ˜ธ๋„ ๋ฝ‘์•„๋‘๋ฉด ๋‚˜์ค‘์— ์ž์‚ฐ ๋งค์นญยท๊ฒ€์ƒ‰์— ์“ธ ์ˆ˜ ์žˆ๋‹ค. ์ •๊ทœ์‹ ํ•˜๋‚˜๋กœ ๋๋‚œ๋‹ค:

CVE_RE = re.compile(r"CVE-\d{4}-\d{4,7}")

def extract_cves(entry: dict) -> str:
    found = CVE_RE.findall(entry["title"] + " " + entry["description"])
    return ",".join(sorted(set(found)))

๊ตฌํ˜„ 4 โ€” ์šด์˜ ์•ˆ์ „์žฅ์น˜ ์„ธ ๊ฐ€์ง€

์ฒซ ์‹คํ–‰ seed ๋ชจ๋“œ. DB๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด ์ ์žฌ๋งŒ ํ•˜๊ณ  ์•Œ๋ฆผ์„ ์ƒ๋žตํ•œ๋‹ค.

first_run = conn.execute("SELECT COUNT(*) FROM advisories").fetchone()[0] == 0
new_rows = scan(conn)
if first_run:
    mark_notified(conn, new_rows)
    print(f"์ฒซ ์‹คํ–‰ seed ์™„๋ฃŒ โ€” {len(new_rows)}๊ฑด ์ ์žฌ (์•Œ๋ฆผ ์ƒ๋žต)")
    return

์†Œ์Šค ๊ฒฉ๋ฆฌ. ์†Œ์Šค ํ•˜๋‚˜์˜ ์žฅ์• ๊ฐ€ ์ „์ฒด ์ˆ˜์ง‘์„ ์ฃฝ์ด๋ฉด ์•ˆ ๋œ๋‹ค. ์†Œ์Šค๋ณ„ try/except๋กœ ๊ฐ์‹ธ๊ณ , ์˜ค๋ฅ˜๋Š” scan_log ํ…Œ์ด๋ธ”์— ๋‚จ๊ฒจ์„œ "์™œ ์˜ค๋Š˜ ์ด ์†Œ์Šค๋งŒ 0๊ฑด์ธ์ง€" ์ถ”์  ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•œ๋‹ค.

์•Œ๋ฆผ ์‹คํŒจ ์‹œ ๋Œ€์ฒด ๊ฒฝ๋กœ. ๋ฉ”์ผ ์„ค์ •์ด ์—†๊ฑฐ๋‚˜ SMTP๊ฐ€ ์‹คํŒจํ•ด๋„ ์ˆ˜์ง‘ ์ž์ฒด๋Š” ์„ฑ๊ณต์ด์–ด์•ผ ํ•œ๋‹ค. ๋ฐœ์†ก ํ•จ์ˆ˜๊ฐ€ False๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ์ฝ˜์†” ์ถœ๋ ฅ์œผ๋กœ ๋Œ€์ฒดํ•œ๋‹ค. ๋ฉ”์ผ์€ smtplib + Gmail ์•ฑ ๋น„๋ฐ€๋ฒˆํ˜ธ(STARTTLS 587)๋ฉด ์ถฉ๋ถ„ํ•˜๊ณ , ์ž๊ฒฉ์ฆ๋ช…์€ ๋ฐ˜๋“œ์‹œ .env๋กœ ๋ถ„๋ฆฌํ•ด .gitignore์— ๋„ฃ๋Š”๋‹ค.

โš ๏ธ .env.example์— ์‹ค์ œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋„ฃ๋Š” ์‚ฌ๊ณ ๊ฐ€ ์˜์™ธ๋กœ ํ”ํ•˜๋‹ค. ์˜ˆ์‹œ ํŒŒ์ผ์—๋Š” your-16char-app-password ๊ฐ™์€ placeholder๋งŒ. ์˜ˆ์‹œ ํŒŒ์ผ์€ ์ปค๋ฐ‹๋˜๋Š” ํŒŒ์ผ์ด๋ผ๋Š” ๊ฑธ ์žŠ์ง€ ๋ง์ž.


์ •๊ธฐ ์‹คํ–‰ โ€” ๊ทธ๋ฆฌ๊ณ  RSS์˜ ์ˆจ์€ ํ•จ์ •

# Linux cron โ€” ๋งค์ผ 09:00
0 9 * * * cd /path/to/watcher && python watcher.py >> data/cron.log 2>&1
# Windows ์ž‘์—… ์Šค์ผ€์ค„๋Ÿฌ
schtasks /Create /TN advisory-watcher /SC DAILY /ST 09:00 `
  /TR "python C:\path\to\watcher.py"

์—ฌ๊ธฐ์„œ ๋งˆ์ง€๋ง‰ ํ•จ์ • ํ•˜๋‚˜. RSS๋Š” ์ตœ๊ทผ N๊ฑด๋งŒ ๋…ธ์ถœํ•œ๋‹ค. ์ด๋ฒˆ์— ํ™•์ธํ•œ ํ”ผ๋“œ๋Š” KISA๊ฐ€ 10๊ฑด, Fortinet์ด 50๊ฑด์ด์—ˆ๋‹ค. KISA ๋ณด์•ˆ๊ณต์ง€๋Š” ํ•˜๋ฃจ์— ์—ฌ๋Ÿฌ ๊ฑด ์˜ฌ๋ผ์˜ค๋Š” ๋‚ ์ด ๋งŽ์œผ๋ฏ€๋กœ, ์‹คํ–‰ ์ฃผ๊ธฐ๊ฐ€ ํ”ผ๋“œ ๋…ธ์ถœ ๊ฐœ์ˆ˜๋ฅผ ๋„˜๊ฒจ๋ฒ„๋ฆฌ๋ฉด ๊ทธ ์‚ฌ์ด ํ•ญ๋ชฉ์€ ์˜์˜ ๋†“์นœ๋‹ค. ์ตœ์†Œ ํ•˜๋ฃจ 1ํšŒ, ๊ณต์ง€๊ฐ€ ๋ชฐ๋ฆฌ๋Š” ์‹œ๊ธฐ์—๋Š” ๋” ์ž์ฃผ ๋Œ๋ฆฌ๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•˜๋‹ค.


๋งˆ์น˜๋ฉฐ

์ž‘์—… ์ž๋™ํ™” ์ „ ์ž๋™ํ™” ํ›„
์•„์นจ ๊ถŒ๊ณ ๋ฌธ ํ™•์ธ ์‚ฌ์ดํŠธ 2๊ณณ ์ˆ˜๋™ ์ˆœํšŒ (๋งค์ผ ~10๋ถ„) ๋ฉ”์ผํ•จ ํ™•์ธ (์‹ ๊ทœ ์žˆ์„ ๋•Œ๋งŒ)
๋†“์นจ ์œ„ํ—˜ ํœด๊ฐ€ยท๋ฐ”์œ ๋‚  = ๊ณต๋ฐฑ ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ๋งค์ผ ์‹คํ–‰
์ด๋ ฅ ๊ด€๋ฆฌ ์—†์Œ (๊ธฐ์–ต์— ์˜์กด) SQLite์— ์ˆ˜์ง‘ยท์•Œ๋ฆผ ์ด๋ ฅ ๋ˆ„์ 
CVE ์ถ”์ถœ ์ˆ˜๋™ ๋ณต์‚ฌ ์ •๊ทœ์‹ ์ž๋™ ์ถ”์ถœยท์ €์žฅ

์ด ์ˆ˜์ง‘๊ธฐ์˜ ๋ณธ์งˆ์€ "RSS ์ฝ๊ธฐ"๊ฐ€ ์•„๋‹ˆ๋ผ ์ƒํƒœ ๊ด€๋ฆฌ๋‹ค. ๋ฌด์—‡์„ ๋ดค๊ณ (UNIQUE ์ œ์•ฝ), ๋ฌด์—‡์„ ์•Œ๋ ธ๊ณ (notified ํ”Œ๋ž˜๊ทธ), ์–ธ์ œ ๋ญ๊ฐ€ ์‹คํŒจํ–ˆ๋Š”์ง€(scan_log)๋ฅผ ๊ธฐ๋กํ•˜๋Š” ์ˆœ๊ฐ„, ์ผํšŒ์„ฑ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋งค์ผ ๋ฏฟ๊ณ  ๋งก๊ธฐ๋Š” ์šด์˜ ๋„๊ตฌ๊ฐ€ ๋œ๋‹ค.

์‹ค๋ฌด ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • ์ˆ˜์ง‘ ์†Œ์Šค์˜ ๊ณต์‹ RSS ํ™•์ธ (์Šคํฌ๋ ˆ์ดํ•‘์€ ์ตœํ›„ ์ˆ˜๋‹จ)
  • fetch์— ์˜ค๋ฅ˜ ๋ถ„๋ฅ˜ ์žฌ์‹œ๋„ (404/4xx/5xxยทํƒ€์ž„์•„์›ƒ์„ ๋‹ค๋ฅด๊ฒŒ)
  • User-Agent ๋ช…์‹œ (๊ธฐ๋ณธ UA ์ฐจ๋‹จ ๋Œ€๋น„)
  • UNIQUE(source, entry_id) โ€” ๊ณ ์œ  ID๋Š” ์†Œ์Šค๋ณ„๋กœ ์ถ”์ถœ ๊ทœ์น™ ์ •์˜
  • ์ฒซ ์‹คํ–‰ seed ๋ชจ๋“œ (์•Œ๋ฆผ ํญํƒ„ ๋ฐฉ์ง€)
  • ์†Œ์Šค ๊ฒฉ๋ฆฌ โ€” ํ•œ ์†Œ์Šค ์žฅ์• ๊ฐ€ ์ „์ฒด๋ฅผ ์ฃฝ์ด์ง€ ์•Š๊ฒŒ
  • ์ž๊ฒฉ์ฆ๋ช…์€ .env ๋ถ„๋ฆฌ + .gitignore (์˜ˆ์‹œ ํŒŒ์ผ์—” placeholder๋งŒ)
  • ์‹คํ–‰ ์ฃผ๊ธฐ < RSS ๋…ธ์ถœ ๊ฐœ์ˆ˜ ์†Œ์ง„ ์†๋„ (์ตœ์†Œ ํ•˜๋ฃจ 1ํšŒ)

๋‹ค์Œ ๊ธ€์—์„œ๋Š” ์ด๋ ‡๊ฒŒ ์ˆ˜์ง‘ํ•œ ๊ถŒ๊ณ ๋ฌธ ๋ฐ์ดํ„ฐ๋ฅผ JSONยทCSV๋กœ ์ •์ œยทํ‘œ์ค€ํ™”ํ•˜๊ณ , ๋ณด์œ  ์ž์‚ฐ ๋ชฉ๋ก๊ณผ ๋งค์นญํ•ด "์šฐ๋ฆฌ์™€ ๊ด€๋ จ ์žˆ๋Š” ๊ถŒ๊ณ ๋ฌธ"๋งŒ ๊ณจ๋ผ๋‚ด๋Š” ๋ฐฉ๋ฒ•์„ ๋‹ค๋ค„๋ณผ ์˜ˆ์ •์ด๋‹ค.

๊ด€๋ จ ๊ธ€



  1. RSS Advisory Board. "RSS 2.0 Specification." https://www.rssboard.org/rss-specificationโ†ฉ
  2. Fortinet. "FortiGuard PSIRT Advisories." https://www.fortiguard.com/psirtโ†ฉ
  3. KISA ๋ณดํ˜ธ๋‚˜๋ผ&KrCERT/CC. "๋ณด์•ˆ๊ณต์ง€." https://www.boho.or.kr/kr/bbs/list.do?bbsId=B0000133&menuNo=205020โ†ฉ

@leekh8
๋ณด์•ˆ, ์›น ๊ฐœ๋ฐœ, Python์„ ๋‹ค๋ฃจ๋Š” ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ