๐Ÿ Python์œผ๋กœ IPยท๋„๋ฉ”์ธ ํ‰ํŒ ์กฐํšŒ ์ž๋™ํ™”ํ•˜๊ธฐ โ€” VirusTotal API ์‹ค์ „ ํ™œ์šฉ

@leekh8 ยท March 30, 2026 ยท 9 min read

๋ณด์•ˆ ์—…๋ฌด๋ฅผ ํ•˜๋‹ค ๋ณด๋ฉด ์˜์‹ฌ์Šค๋Ÿฌ์šด IP๋‚˜ ๋„๋ฉ”์ธ์„ ๋งˆ์ฃผ์น˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๋‹ค. "์ด IP๊ฐ€ ์•…์„ฑ์ธ์ง€ ์•„๋‹Œ์ง€" ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ๋งค๋ฒˆ VirusTotal ์‚ฌ์ดํŠธ์— ์ง์ ‘ ๋“ค์–ด๊ฐ€์„œ ๋ณต๋ถ™ํ•˜๋Š” ์ž‘์—…์ด ๋ฐ˜๋ณต๋œ๋‹ค.

ํ•˜๋ฃจ์— ์ˆ˜์‹ญ ๊ฐœ๋ฅผ ํ™•์ธํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์ด๋ผ๋ฉด? ๊ทธ๋ƒฅ ์†์œผ๋กœ ํ•˜๋Š” ๊ฑด ํ•œ๊ณ„๊ฐ€ ์žˆ๋‹ค.

์ด ๊ธ€์—์„œ๋Š” Python์œผ๋กœ VirusTotal API๋ฅผ ํ˜ธ์ถœํ•ด์„œ IPยท๋„๋ฉ”์ธยทํŒŒ์ผ ํ•ด์‹œ์˜ ์•…์„ฑ ์—ฌ๋ถ€๋ฅผ ์ž๋™์œผ๋กœ ์กฐํšŒํ•˜๊ณ , ๊ฒฐ๊ณผ๋ฅผ Slack์œผ๋กœ ๋ฐœ์†กํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋งŒ๋“ค์–ด๋ณธ๋‹ค. ๋ณด์•ˆ ์ž๋™ํ™”์— ์ž…๋ฌธํ•˜๋ ค๋Š” ์‚ฌ๋žŒ์ด๋ผ๋ฉด ์ด ๊ธ€ ํ•˜๋‚˜๋กœ ์‹ค์ „์—์„œ ๋ฐ”๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ๋ฅผ ๊ฐ€์ ธ๊ฐˆ ์ˆ˜ ์žˆ๋‹ค.


์™œ Python์ธ๊ฐ€?

๋ณด์•ˆ ์ž๋™ํ™” ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค ๋•Œ Python์ด ์‚ฌ์‹ค์ƒ ํ‘œ์ค€์ฒ˜๋Ÿผ ์“ฐ์ด๋Š” ๋ฐ๋Š” ์ด์œ ๊ฐ€ ์žˆ๋‹ค.

requests, json, re ๊ฐ™์€ ๊ธฐ๋ณธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒ์œผ๋กœ๋„ HTTP API ํ˜ธ์ถœ, ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ, ์ •๊ทœ์‹ ์ฒ˜๋ฆฌ๊ฐ€ ์ „๋ถ€ ๊ฐ€๋Šฅํ•˜๋‹ค. ๋ณ„๋„์˜ ๋ณต์žกํ•œ ๋นŒ๋“œ ํ™˜๊ฒฝ ์—†์ด ์Šคํฌ๋ฆฝํŠธ ํ•˜๋‚˜๋กœ ๋๋‚œ๋‹ค.

๋ณด์•ˆ ๋ถ„์•ผ์—์„œ Python์ด ๊ฐ•์„ธ์ธ ๋˜ ๋‹ค๋ฅธ ์ด์œ ๋Š” ์ƒํƒœ๊ณ„๋‹ค. MISP, Cortex, TheHive, ๊ทธ๋ฆฌ๊ณ  ๊ฐ์ข… SOAR ํ”Œ๋žซํผ์˜ ์ปค๋„ฅํ„ฐ๊ฐ€ Python์œผ๋กœ ์ž‘์„ฑ๋˜์–ด ์žˆ๋‹ค. VirusTotal, Shodan, AbuseIPDB, AlienVault OTX ๊ฐ™์€ ์œ„ํ˜‘ ์ธํ…”๋ฆฌ์ „์Šค ํ”Œ๋žซํผ๋„ Python SDK๋ฅผ ๊ณต์‹ ์ง€์›ํ•œ๋‹ค.1

์ฆ‰, Python์„ ๋ฐฐ์šฐ๋ฉด ๋ณด์•ˆ ์ž๋™ํ™”์˜ ์ง„์ž… ์žฅ๋ฒฝ์ด ํ›จ์”ฌ ๋‚ฎ์•„์ง„๋‹ค.


VirusTotal์ด๋ž€?

VirusTotal์€ Google์ด ์ธ์ˆ˜ํ•œ ์œ„ํ˜‘ ์ธํ…”๋ฆฌ์ „์Šค ํ”Œ๋žซํผ์ด๋‹ค. 70๊ฐœ ์ด์ƒ์˜ ๋ณด์•ˆ ๋ฒค๋”(Kaspersky, McAfee, CrowdStrike ๋“ฑ)์˜ ์—”์ง„์ด ๋ชจ์—ฌ ์žˆ์–ด์„œ, ํŒŒ์ผ ํ•˜๋‚˜๋ฅผ ์˜ฌ๋ฆฌ๋ฉด ๋ชจ๋“  ์—”์ง„์œผ๋กœ ๋™์‹œ์— ๊ฒ€์‚ฌํ•œ๋‹ค.2

์ฃผ์š” ์กฐํšŒ ๋Œ€์ƒ์€ ์„ธ ๊ฐ€์ง€๋‹ค:

๋Œ€์ƒ ์˜ˆ์‹œ ํ™œ์šฉ ์‚ฌ๋ก€
IP ์ฃผ์†Œ 8.8.8.8 ์•…์„ฑ C2 ์„œ๋ฒ„ ์—ฌ๋ถ€ ํ™•์ธ
๋„๋ฉ”์ธ malware-site.com ํ”ผ์‹ฑ ๋„๋ฉ”์ธ ํ™•์ธ
ํŒŒ์ผ ํ•ด์‹œ MD5, SHA-256 ์•…์„ฑ ํŒŒ์ผ ์—ฌ๋ถ€ ํ™•์ธ

๋ฌด๋ฃŒ ๊ณ„์ •์œผ๋กœ ํ•˜๋ฃจ 500ํšŒ, ๋ถ„๋‹น 4ํšŒ ์กฐํšŒ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค. ์†Œ๊ทœ๋ชจ ์ž๋™ํ™”์—๋Š” ์ถฉ๋ถ„ํ•˜๋‹ค.3


์ค€๋น„

API ํ‚ค ๋ฐœ๊ธ‰

  1. VirusTotal ํšŒ์›๊ฐ€์ž…
  2. ์šฐ์ธก ์ƒ๋‹จ ํ”„๋กœํ•„ โ†’ API key ํด๋ฆญ
  3. API ํ‚ค ๋ณต์‚ฌ

ํŒจํ‚ค์ง€ ์„ค์น˜

pip install requests python-dotenv

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

vt-checker/
โ”œโ”€โ”€ .env           โ† API ํ‚ค ์ €์žฅ (์ ˆ๋Œ€ ์ปค๋ฐ‹ ๊ธˆ์ง€)
โ”œโ”€โ”€ checker.py     โ† ๋ฉ”์ธ ์Šคํฌ๋ฆฝํŠธ
โ””โ”€โ”€ .gitignore

.env ํŒŒ์ผ:

VT_API_KEY=your_virustotal_api_key_here
SLACK_WEBHOOK_URL=your_slack_webhook_url_here

VirusTotal API ๊ธฐ๋ณธ ๊ตฌ์กฐ

VirusTotal API v3๋Š” REST ๋ฐฉ์‹์ด๋‹ค. ๊ณตํ†ต ๊ตฌ์กฐ๋Š” ์ด๋ ‡๋‹ค:

GET https://www.virustotal.com/api/v3/{๋ฆฌ์†Œ์Šค ํƒ€์ž…}/{์‹๋ณ„์ž}

์˜ˆ์‹œ:

์กฐํšŒ ๋Œ€์ƒ ์—”๋“œํฌ์ธํŠธ
IP ์ฃผ์†Œ GET /api/v3/ip_addresses/{ip}
๋„๋ฉ”์ธ GET /api/v3/domains/{domain}
ํŒŒ์ผ ํ•ด์‹œ GET /api/v3/files/{hash}

์ธ์ฆ์€ ํ—ค๋”์— API ํ‚ค๋ฅผ ๋„ฃ๋Š” ๋ฐฉ์‹์ด๋‹ค:

headers = {
    "x-apikey": "YOUR_API_KEY"
}

์‘๋‹ต ๊ตฌ์กฐ๋Š” JSON์ด๊ณ , ํ•ต์‹ฌ์€ last_analysis_stats ํ•„๋“œ๋‹ค:

{
  "data": {
    "attributes": {
      "last_analysis_stats": {
        "malicious": 5,
        "suspicious": 1,
        "undetected": 60,
        "harmless": 8,
        "timeout": 0
      }
    }
  }
}

malicious ๊ฐ’์ด ๋†’์„์ˆ˜๋ก ์•…์„ฑ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ 3 ์ด์ƒ์ด๋ฉด ์•…์„ฑ์œผ๋กœ ํŒ๋‹จํ•˜๋Š” ๊ธฐ์ค€์„ ๋งŽ์ด ์“ด๋‹ค.


์ฝ”๋“œ ์ž‘์„ฑ

1๋‹จ๊ณ„: ๊ธฐ๋ณธ IP ์กฐํšŒ

import requests
import os
from dotenv import load_dotenv

load_dotenv()

VT_API_KEY = os.getenv("VT_API_KEY")
BASE_URL = "https://www.virustotal.com/api/v3"
HEADERS = {"x-apikey": VT_API_KEY}


def check_ip(ip: str) -> dict:
    """IP ์ฃผ์†Œ ์•…์„ฑ ์—ฌ๋ถ€ ์กฐํšŒ"""
    url = f"{BASE_URL}/ip_addresses/{ip}"
    response = requests.get(url, headers=HEADERS)

    if response.status_code == 200:
        data = response.json()
        stats = data["data"]["attributes"]["last_analysis_stats"]
        return {
            "type": "ip",
            "indicator": ip,
            "malicious": stats.get("malicious", 0),
            "suspicious": stats.get("suspicious", 0),
            "harmless": stats.get("harmless", 0),
            "undetected": stats.get("undetected", 0),
        }
    elif response.status_code == 404:
        return {"type": "ip", "indicator": ip, "error": "Not found"}
    else:
        return {"type": "ip", "indicator": ip, "error": f"HTTP {response.status_code}"}

2๋‹จ๊ณ„: ๋„๋ฉ”์ธ๊ณผ ํ•ด์‹œ ์ถ”๊ฐ€

๊ตฌ์กฐ๊ฐ€ ๊ฑฐ์˜ ๊ฐ™์•„์„œ ํ•จ์ˆ˜๋ฅผ ๋ฌถ์–ด์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค:

def check_domain(domain: str) -> dict:
    """๋„๋ฉ”์ธ ์•…์„ฑ ์—ฌ๋ถ€ ์กฐํšŒ"""
    url = f"{BASE_URL}/domains/{domain}"
    return _parse_response(url, "domain", domain)


def check_hash(file_hash: str) -> dict:
    """ํŒŒ์ผ ํ•ด์‹œ ์•…์„ฑ ์—ฌ๋ถ€ ์กฐํšŒ"""
    url = f"{BASE_URL}/files/{file_hash}"
    return _parse_response(url, "hash", file_hash)


def _parse_response(url: str, indicator_type: str, indicator: str) -> dict:
    """๊ณตํ†ต ์‘๋‹ต ํŒŒ์‹ฑ"""
    response = requests.get(url, headers=HEADERS)

    if response.status_code == 200:
        stats = (
            response.json()["data"]["attributes"]["last_analysis_stats"]
        )
        return {
            "type": indicator_type,
            "indicator": indicator,
            "malicious": stats.get("malicious", 0),
            "suspicious": stats.get("suspicious", 0),
            "harmless": stats.get("harmless", 0),
            "undetected": stats.get("undetected", 0),
        }
    elif response.status_code == 404:
        return {"type": indicator_type, "indicator": indicator, "error": "Not found"}
    else:
        return {
            "type": indicator_type,
            "indicator": indicator,
            "error": f"HTTP {response.status_code}",
        }

3๋‹จ๊ณ„: ์•…์„ฑ ํŒ๋‹จ ๋กœ์ง

๋‹จ์ˆœํžˆ ์ˆซ์ž๋งŒ ๋ณด๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ํŒ๋‹จ ๊ธฐ์ค€์„ ๋ช…ํ™•ํžˆ ์ •์˜ํ•ด๋‘๋Š” ๊ฒŒ ์ข‹๋‹ค:

def judge(result: dict) -> str:
    """
    malicious >= 5  โ†’ MALICIOUS (์•…์„ฑ)
    malicious >= 1  โ†’ SUSPICIOUS (์˜์‹ฌ)
    malicious == 0  โ†’ CLEAN (์ •์ƒ)
    """
    if "error" in result:
        return "UNKNOWN"

    malicious = result.get("malicious", 0)
    suspicious = result.get("suspicious", 0)

    if malicious >= 5:
        return "MALICIOUS"
    if malicious >= 1 or suspicious >= 3:
        return "SUSPICIOUS"
    return "CLEAN"

์ž„๊ณ„๊ฐ’(5, 1, 3)์€ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ์กฐ์ •ํ•˜๋ฉด ๋œ๋‹ค. ๋‚ด๋ถ€๋ง IP๋ฅผ ์กฐํšŒํ•  ๋•Œ๋Š” false positive๊ฐ€ ๋งŽ์œผ๋‹ˆ ์ž„๊ณ„๊ฐ’์„ ๋†’์ด๋Š” ์‹์œผ๋กœ.

4๋‹จ๊ณ„: Slack ์•Œ๋ฆผ

import json

SLACK_WEBHOOK_URL = os.getenv("SLACK_WEBHOOK_URL")

VERDICT_EMOJI = {
    "MALICIOUS":  ":red_circle:",
    "SUSPICIOUS": ":large_yellow_circle:",
    "CLEAN":      ":white_check_mark:",
    "UNKNOWN":    ":white_circle:",
}

VERDICT_COLOR = {
    "MALICIOUS":  "#e74c3c",
    "SUSPICIOUS": "#e67e22",
    "CLEAN":      "#27ae60",
    "UNKNOWN":    "#7f8c8d",
}


def send_slack(result: dict, verdict: str) -> None:
    """Slack ์›นํ›…์œผ๋กœ ์กฐํšŒ ๊ฒฐ๊ณผ ๋ฐœ์†ก"""
    if not SLACK_WEBHOOK_URL:
        print("[!] SLACK_WEBHOOK_URL ๋ฏธ์„ค์ • โ€” Slack ์•Œ๋ฆผ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.")
        return

    emoji = VERDICT_EMOJI.get(verdict, ":white_circle:")
    color = VERDICT_COLOR.get(verdict, "#7f8c8d")
    indicator = result["indicator"]

    if "error" in result:
        text = f"{emoji} *{indicator}* โ€” ์กฐํšŒ ์‹คํŒจ: {result['error']}"
        fields = []
    else:
        text = f"{emoji} *{indicator}* โ€” {verdict}"
        fields = [
            {"title": "Malicious",  "value": str(result["malicious"]),  "short": True},
            {"title": "Suspicious", "value": str(result["suspicious"]), "short": True},
            {"title": "Harmless",   "value": str(result["harmless"]),   "short": True},
            {"title": "Undetected", "value": str(result["undetected"]), "short": True},
        ]

    payload = {
        "attachments": [
            {
                "color": color,
                "text": text,
                "fields": fields,
                "footer": "VirusTotal API",
            }
        ]
    }

    requests.post(
        SLACK_WEBHOOK_URL,
        data=json.dumps(payload),
        headers={"Content-Type": "application/json"},
    )

5๋‹จ๊ณ„: ์ „์ฒด ํ†ตํ•ฉ

import time

def check_and_notify(indicator: str) -> None:
    """ํƒ€์ž… ์ž๋™ ๊ฐ์ง€ ํ›„ ์กฐํšŒ ๋ฐ ์•Œ๋ฆผ"""
    import re

    # ํƒ€์ž… ํŒ๋‹จ
    ip_pattern = re.compile(
        r"^(\d{1,3}\.){3}\d{1,3}$"
    )
    hash_pattern = re.compile(
        r"^[a-fA-F0-9]{32}$|^[a-fA-F0-9]{40}$|^[a-fA-F0-9]{64}$"
    )

    indicator = indicator.strip()

    if ip_pattern.match(indicator):
        result = check_ip(indicator)
    elif hash_pattern.match(indicator):
        result = check_hash(indicator)
    else:
        result = check_domain(indicator)

    verdict = judge(result)
    print(f"[{verdict}] {indicator}")
    send_slack(result, verdict)


def batch_check(indicators: list, delay: float = 15.0) -> None:
    """
    ์—ฌ๋Ÿฌ ์ง€ํ‘œ ์ผ๊ด„ ์กฐํšŒ
    delay: ์š”์ฒญ ๊ฐ„๊ฒฉ (๋ฌด๋ฃŒ ๊ณ„์ •์€ ๋ถ„๋‹น 4ํšŒ = ์ตœ์†Œ 15์ดˆ)
    """
    for i, indicator in enumerate(indicators):
        print(f"[{i+1}/{len(indicators)}] {indicator} ์กฐํšŒ ์ค‘...")
        check_and_notify(indicator)
        if i < len(indicators) - 1:
            time.sleep(delay)


# โ”€โ”€ ์‹คํ–‰ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if __name__ == "__main__":
    targets = [
        "8.8.8.8",
        "185.220.101.45",          # ์•Œ๋ ค์ง„ Tor exit node
        "malware-test.com",
        "44d88612fea8a8f36de82e1278abb02f",  # EICAR ํ…Œ์ŠคํŠธ ํ•ด์‹œ (MD5)
    ]
    batch_check(targets)

์ „์ฒด ๋™์ž‘ ํ๋ฆ„

IP ํŒจํ„ด
๋„๋ฉ”์ธ
ํ•ด์‹œ ํŒจํ„ด
malicious >= 5
malicious >= 1
malicious == 0
์ž…๋ ฅ

IP / ๋„๋ฉ”์ธ / ํ•ด์‹œ
ํƒ€์ž… ์ž๋™ ๊ฐ์ง€

(์ •๊ทœ์‹ ํŒจํ„ด ๋งค์นญ)
IP ์กฐํšŒ

/ip_addresses/{ip}
๋„๋ฉ”์ธ ์กฐํšŒ

/domains/{domain}
ํ•ด์‹œ ์กฐํšŒ

/files/{hash}
VirusTotal API

์‘๋‹ต ์ˆ˜์‹ 
ํŒ๋‹จ ๋กœ์ง

malicious ์ˆ˜์น˜ ๋ถ„์„
MALICIOUS
SUSPICIOUS
CLEAN
Slack ์•Œ๋ฆผ ๋ฐœ์†ก

(์ƒ‰์ƒ ๊ตฌ๋ถ„)


์‹คํ–‰ ๊ฒฐ๊ณผ ์˜ˆ์‹œ

[1/4] 8.8.8.8 ์กฐํšŒ ์ค‘...
[CLEAN] 8.8.8.8

[2/4] 185.220.101.45 ์กฐํšŒ ์ค‘...
[MALICIOUS] 185.220.101.45

[3/4] malware-test.com ์กฐํšŒ ์ค‘...
[SUSPICIOUS] malware-test.com

[4/4] 44d88612fea8a8f36de82e1278abb02f ์กฐํšŒ ์ค‘...
[MALICIOUS] 44d88612fea8a8f36de82e1278abb02f

Slack์—๋Š” ์ด๋Ÿฐ ์‹์œผ๋กœ ๋ฐœ์†ก๋œ๋‹ค:

  • ๐Ÿ”ด 185.220.101.45 โ€” MALICIOUS | Malicious: 18 | Suspicious: 3
  • ๐ŸŸก malware-test.com โ€” SUSPICIOUS | Malicious: 2 | Suspicious: 1
  • โœ… 8.8.8.8 โ€” CLEAN | Malicious: 0 | Harmless: 76

Rate Limit ์ฒ˜๋ฆฌ

๋ฌด๋ฃŒ ๊ณ„์ •์€ ๋ถ„๋‹น 4ํšŒ, ํ•˜๋ฃจ 500ํšŒ ์ œํ•œ์ด ์žˆ๋‹ค.3 ์ด๋ฅผ ์–ด๊ธฐ๋ฉด 429 Too Many Requests๊ฐ€ ๋Œ์•„์˜จ๋‹ค.

์œ„ ์ฝ”๋“œ์—์„œ delay=15.0์œผ๋กœ ์„ค์ •ํ•œ ๊ฒŒ ๊ทธ ์ด์œ ๋‹ค. 15์ดˆ ๊ฐ„๊ฒฉ์ด๋ฉด ๋ถ„๋‹น 4ํšŒ ์ด๋‚ด๋‹ค.

๋” ๊ฒฌ๊ณ ํ•˜๊ฒŒ ๋งŒ๋“ค๋ ค๋ฉด ์žฌ์‹œ๋„ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค:

def safe_request(url: str, headers: dict, retries: int = 3) -> requests.Response:
    """429 ๋ฐœ์ƒ ์‹œ ์ž๋™ ์žฌ์‹œ๋„"""
    for attempt in range(retries):
        response = requests.get(url, headers=headers)
        if response.status_code == 429:
            wait = 60 * (attempt + 1)
            print(f"[!] Rate limit ๋„๋‹ฌ. {wait}์ดˆ ๋Œ€๊ธฐ ํ›„ ์žฌ์‹œ๋„...")
            time.sleep(wait)
        else:
            return response
    return response

ํ™•์žฅ ์•„์ด๋””์–ด

์ด ์Šคํฌ๋ฆฝํŠธ๋Š” ์‹œ์ž‘์ ์ด๋‹ค. ์—ฌ๊ธฐ์„œ ์—ฌ๋Ÿฌ ๋ฐฉํ–ฅ์œผ๋กœ ํ™•์žฅํ•  ์ˆ˜ ์žˆ๋‹ค.

1. CSV/ํ…์ŠคํŠธ ํŒŒ์ผ ์ž…๋ ฅ

๋ถ„์„ํ•  ์ง€ํ‘œ๊ฐ€ ๋งŽ์„ ๋•Œ:

with open("indicators.txt", "r") as f:
    targets = [line.strip() for line in f if line.strip()]
batch_check(targets)

2. ๊ฒฐ๊ณผ๋ฅผ CSV๋กœ ์ €์žฅ

import csv
import datetime

def save_to_csv(results: list, filename: str = None) -> None:
    if not filename:
        today = datetime.date.today().isoformat()
        filename = f"vt_results_{today}.csv"

    with open(filename, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(
            f,
            fieldnames=["indicator", "type", "verdict",
                        "malicious", "suspicious", "harmless", "undetected"]
        )
        writer.writeheader()
        writer.writerows(results)
    print(f"[*] ๊ฒฐ๊ณผ ์ €์žฅ: {filename}")

3. SOAR ํ”Œ๋ ˆ์ด๋ถ๊ณผ ์—ฐ๋™

SOAR ํ”Œ๋žซํผ(FortiSOAR, Palo Alto XSOAR ๋“ฑ)์—์„œ Python ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ”Œ๋ ˆ์ด๋ถ ์Šคํ…์œผ๋กœ ์‚ฝ์ž…ํ•  ์ˆ˜ ์žˆ๋‹ค. ์œ„ ์ฝ”๋“œ์—์„œ check_and_notify() ํ•จ์ˆ˜๋ฅผ ๊ทธ๋Œ€๋กœ ์ปค๋„ฅํ„ฐ ์•ก์…˜์œผ๋กœ ๋ž˜ํ•‘ํ•˜๋ฉด ๋œ๋‹ค.

์ธ์‹œ๋˜ํŠธ์—์„œ ์ถ”์ถœํ•œ IOC๋ฅผ ํ”Œ๋ ˆ์ด๋ถ์ด ์ž๋™์œผ๋กœ ์ด ํ•จ์ˆ˜์— ๋„˜๊ธฐ๊ณ , ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ์ฐจ๋‹จ/์—์Šค์ปฌ๋ ˆ์ด์…˜์„ ๊ฒฐ์ •ํ•˜๋Š” ์‹์ด๋‹ค. ํ”Œ๋ ˆ์ด๋ถ ํ•˜๋‚˜๋กœ IOC ์ˆ˜์ง‘๋ถ€ํ„ฐ ํ‰ํŒ ์กฐํšŒ, ๋Œ€์‘๊นŒ์ง€ ์ž๋™ํ™”๋˜๋Š” ๊ตฌ์กฐ๋‹ค.


์ฃผ์˜์‚ฌํ•ญ

API ํ‚ค๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ์ง์ ‘ ๋„ฃ์ง€ ์•Š๋Š”๋‹ค. .env ํŒŒ์ผ์„ ์‚ฌ์šฉํ•˜๊ณ , .gitignore์— ๋ฐ˜๋“œ์‹œ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค:

# .gitignore
.env
*.env

GitHub์— API ํ‚ค๊ฐ€ ์˜ฌ๋ผ๊ฐ€๋ฉด ์ž๋™์œผ๋กœ ๊ฐ์ง€๋˜์–ด ํ‚ค๊ฐ€ ์ฆ‰์‹œ ๋ฌดํšจํ™”๋˜๊ณ  ์ด๋ฉ”์ผ ์•Œ๋ฆผ์ด ์˜จ๋‹ค. ํ•˜์ง€๋งŒ ํ‚ค๊ฐ€ ๋…ธ์ถœ๋˜๋Š” ์‹œ๊ฐ„ ๋™์•ˆ ๋ฌด๋‹จ ์‚ฌ์šฉ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์œผ๋‹ˆ ์ฒ˜์Œ๋ถ€ํ„ฐ ์กฐ์‹ฌํ•˜๋Š” ๊ฒŒ ๋งž๋‹ค.


๋งˆ์น˜๋ฉฐ

์ด ์Šคํฌ๋ฆฝํŠธ ํ•˜๋‚˜๋กœ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋“ค์„ ์ •๋ฆฌํ•˜๋ฉด ์ด๋ ‡๋‹ค:

์ž‘์—… ์ž๋™ํ™” ์ „ ์ž๋™ํ™” ํ›„
IP 10๊ฐœ ์กฐํšŒ ์ˆ˜๋™ ๋ณต๋ถ™ 10ํšŒ (์•ฝ 5๋ถ„) ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ 1ํšŒ (์•ฝ 2.5๋ถ„ ๋Œ€๊ธฐ)
๊ฒฐ๊ณผ ๊ธฐ๋ก ์ง์ ‘ ์—‘์…€ ์ž‘์„ฑ CSV ์ž๋™ ์ €์žฅ
ํŒ€ ๊ณต์œ  ์ด๋ฉ”์ผ/๋ฉ”์‹œ์ง€ ์ง์ ‘ ๋ฐœ์†ก Slack ์ž๋™ ๋ฐœ์†ก

๋ณด์•ˆ ์ž๋™ํ™”์˜ ํ•ต์‹ฌ์€ ์‚ฌ๋žŒ์ด ํŒ๋‹จํ•ด์•ผ ํ•˜๋Š” ๊ฒƒ๊ณผ ๊ธฐ๊ณ„๊ฐ€ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” ๊ฒƒ์„ ๊ตฌ๋ถ„ํ•˜๋Š” ๋ฐ ์žˆ๋‹ค. IOC ํ‰ํŒ ์กฐํšŒ์ฒ˜๋Ÿผ ๋ฐ˜๋ณต์ ์ด๊ณ  ๊ทœ์น™ ๊ธฐ๋ฐ˜์ธ ์ž‘์—…์€ ์ž๋™ํ™”ํ•˜๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ํ•œ ์‹ค์ œ ํŒ๋‹จ๊ณผ ๋Œ€์‘์— ์‚ฌ๋žŒ์˜ ์‹œ๊ฐ„์„ ์“ฐ๋Š” ๊ฒƒ์ด ๋งž๋‹ค.

๋‹ค์Œ ๊ธ€์—์„œ๋Š” ์ด๋ฒˆ์— ๋งŒ๋“  ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ™•์žฅํ•ด์„œ AbuseIPDB, AlienVault OTX ๊ฐ™์€ ์ถ”๊ฐ€ ์œ„ํ˜‘ ์ธํ…”๋ฆฌ์ „์Šค ์†Œ์Šค์™€ ๋ฉ€ํ‹ฐ์†Œ์Šค ์กฐํšŒ๋ฅผ ํ†ตํ•ฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋‹ค๋ค„๋ณผ ์˜ˆ์ •์ด๋‹ค.



  1. VirusTotal. "VirusTotal API v3 Overview." VirusTotal Developer Documentation. https://developers.virustotal.com/reference/overviewโ†ฉ
  2. VirusTotal. "How it works." VirusTotal. https://www.virustotal.com/gui/how-it-worksโ†ฉ
  3. VirusTotal. "API Rate Limiting." VirusTotal Developer Documentation. https://developers.virustotal.com/reference/public-vs-premium-apiโ†ฉ

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