Ver3.0 ๐ก๏ธ OWASP ZAP ์๋ํ ์คํฌ๋ฆฝํ ์ผ๋ก ๋ณด์ ํ ์คํธ ๋ง์คํฐํ๊ธฐ

๐ก๏ธ OWASP ZAP ์๋ํ ์คํฌ๋ฆฝํ ์ผ๋ก ๋ณด์ ํ ์คํธ ๋ง์คํฐํ๊ธฐ
์น๊ตฌ์ฒ๋ผ ์ฝ๊ฒ ๋ฐฐ์ฐ๋ ์น ๋ณด์ ์๋ํ์ ๋ชจ๋ ๊ฒ
๐ฏ ์ด ๊ธ์์ ๋ฐฐ์ธ ๋ด์ฉ
์น ์ ํ๋ฆฌ์ผ์ด์ ๋ณด์ ํ ์คํธ๋ฅผ ์๋ํํ๊ณ ์ถ์๋ฐ ์ด๋์๋ถํฐ ์์ํด์ผ ํ ์ง ๋ง๋งํ๋ ์ ์์ง? OWASP ZAP(Zed Attack Proxy)์ ์ธ๊ณ์์ ๊ฐ์ฅ ์ธ๊ธฐ ์๋ ์คํ์์ค ์น ๋ณด์ ์ค์บ๋์ผ. ์ด ๊ธ์์๋ ZAP์ ์๋ํ ์คํฌ๋ฆฝํ ๊ธฐ๋ฅ์ ํ์ฉํด์ ๋ฐ๋ณต์ ์ธ ๋ณด์ ํ ์คํธ๋ฅผ ํจ์จ์ ์ผ๋ก ์ํํ๋ ๋ฐฉ๋ฒ์ ์น๊ตฌ์๊ฒ ์ค๋ช ํ๋ฏ ์ฝ๊ฒ ์๋ ค์ค๊ฒ! ๐ช
๐ OWASP ZAP์ด ๋ญ๊ธธ๋?
OWASP ZAP์ Open Web Application Security Project์์ ๋ง๋ ๋ฌด๋ฃ ๋ณด์ ํ ์คํ ๋๊ตฌ์ผ. ์ฝ๊ฒ ๋งํ๋ฉด ์น์ฌ์ดํธ๋ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ณด์ ์ทจ์ฝ์ ์ ์ฐพ์์ฃผ๋ ์น๊ตฌ๋ผ๊ณ ์๊ฐํ๋ฉด ๋ผ. ๐ต๏ธ
์ผ๋ฐ์ ์ผ๋ก ๋ณด์ ํ ์คํธ๋ฅผ ์๋์ผ๋ก ํ๋ ค๋ฉด ์์ฒญ๋ ์๊ฐ๊ณผ ๋ ธ๋ ฅ์ด ํ์ํด. ๋งค๋ฒ ๊ฐ์ ํ ์คํธ๋ฅผ ๋ฐ๋ณตํ๋ ๊ฑด ์ ๋ง ์ง๋ฃจํ๊ณ ๋นํจ์จ์ ์ด์ง. ๊ทธ๋์ ๋ฑ์ฅํ ๊ฒ ๋ฐ๋ก ์๋ํ ์คํฌ๋ฆฝํ ์ด์ผ!
๐ก ์ ZAP์ ์ ํํด์ผ ํ ๊น?
โ
์์ ๋ฌด๋ฃ ์คํ์์ค
โ
ํ๋ฐํ ์ปค๋ฎค๋ํฐ ์ง์
โ
๋ค์ํ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด ์ง์ (Python, Java, Node.js ๋ฑ)
โ
CI/CD ํ์ดํ๋ผ์ธ ํตํฉ ๊ฐ๋ฅ
โ
์ด๋ณด์๋ถํฐ ์ ๋ฌธ๊ฐ๊น์ง ์ฌ์ฉ ๊ฐ๋ฅํ ์ธํฐํ์ด์ค
๐ ZAP ์๋ํ์ ํต์ฌ ๊ฐ๋
ZAP ์๋ํ๋ฅผ ์ดํดํ๋ ค๋ฉด ๋จผ์ ๋ช ๊ฐ์ง ํต์ฌ ๊ฐ๋ ์ ์์์ผ ํด. ์ด๋ ต์ง ์์ผ๋๊น ์ฒ์ฒํ ๋ฐ๋ผ์๋ด! ๐
1. ZAP API - ์๋ํ์ ์์์
ZAP API๋ ZAP์ ๋ชจ๋ ๊ธฐ๋ฅ์ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ์ ์ดํ ์ ์๊ฒ ํด์ฃผ๋ ์ธํฐํ์ด์ค์ผ. REST API ํํ๋ก ์ ๊ณต๋๊ธฐ ๋๋ฌธ์ ์ด๋ค ํ๋ก๊ทธ๋๋ฐ ์ธ์ด์์๋ HTTP ์์ฒญ๋ง ๋ณด๋ผ ์ ์์ผ๋ฉด ์ฌ์ฉํ ์ ์์ด.
๐ ZAP API์ ์ฃผ์ ๊ตฌ์ฑ ์์
Core API: ZAP์ ๊ธฐ๋ณธ ๊ธฐ๋ฅ ์ ์ด (์ค์บ ์์/์ค์ง, ๊ฒฐ๊ณผ ์กฐํ ๋ฑ)
Spider API: ์น์ฌ์ดํธ ํฌ๋กค๋ง ๊ธฐ๋ฅ
Active Scan API: ๋ฅ๋์ ๋ณด์ ์ค์บ ์ํ
Passive Scan API: ์๋์ ๋ณด์ ๋ถ์
Ajax Spider API: JavaScript ๊ธฐ๋ฐ ์ ํ๋ฆฌ์ผ์ด์
ํฌ๋กค๋ง
Authentication API: ์ธ์ฆ ๊ด๋ จ ์ค์
2. ZAP ๋ฐ๋ชฌ ๋ชจ๋
์๋ํ๋ฅผ ์ํด์๋ ZAP์ ๋ฐ๋ชฌ(๋ฐฑ๊ทธ๋ผ์ด๋) ๋ชจ๋๋ก ์คํํด์ผ ํด. GUI ์์ด ๋ช ๋ น์ค์์ ์คํ๋๋ ๋ฐฉ์์ด์ง. ์ด๋ ๊ฒ ํ๋ฉด ์๋ฒ ํ๊ฒฝ์ด๋ CI/CD ํ์ดํ๋ผ์ธ์์๋ ZAP์ ์ฌ์ฉํ ์ ์์ด! ๐ฅ๏ธ
# ZAP ๋ฐ๋ชฌ ๋ชจ๋๋ก ์คํํ๊ธฐ
zap.sh -daemon -host 0.0.0.0 -port 8080 -config api.key=your-api-key-here
3. ์ปจํ ์คํธ(Context)์ ์ธ์ (Session)
ZAP์์ ์ปจํ ์คํธ๋ ํ ์คํธํ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฒ์๋ฅผ ์ ์ํ๋ ๊ฑฐ์ผ. ์ด๋ค URL์ ํฌํจํ๊ณ ์ ์ธํ ์ง, ์ธ์ฆ์ ์ด๋ป๊ฒ ํ ์ง ๋ฑ์ ์ค์ ํ์ง. ์ธ์ ์ ์ด๋ฐ ์ค์ ๋ค์ ์ ์ฅํ๊ณ ์ฌ์ฌ์ฉํ ์ ์๊ฒ ํด์ค.
๐ ๏ธ ํ๊ฒฝ ์ค์ ํ๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ZAP ์๋ํ ํ๊ฒฝ์ ๊ตฌ์ถํด๋ณด์! ๋จ๊ณ๋ณ๋ก ์ฐจ๊ทผ์ฐจ๊ทผ ์งํํ ๊ฒ. ๐
Step 1: ZAP ์ค์น
ZAP์ Windows, macOS, Linux ๋ชจ๋์์ ์ฌ์ฉํ ์ ์์ด. ๊ณต์ ์น์ฌ์ดํธ(www.zaproxy.org)์์ ๋ค์ด๋ก๋ํ ์ ์์ง.
๐ช Windows
์ค์น ํ์ผ(.exe)์ ๋ค์ด๋ก๋ํด์ ์คํํ๋ฉด ๋! ๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฉ๋ฒ์ด์ผ.
๐ macOS
DMG ํ์ผ์ ๋ค์ด๋ก๋ํ๊ฑฐ๋ Homebrew๋ฅผ ์ฌ์ฉํ ์ ์์ด:
brew install --cask owasp-zap
๐ง Linux
ํจํค์ง ๋งค๋์ ๋ ์ง์ ๋ค์ด๋ก๋:
sudo apt install zaproxy
Step 2: Python ํ๊ฒฝ ๊ตฌ์ฑ
Python์ผ๋ก ZAP์ ์ ์ดํ๋ ๊ฒ ๊ฐ์ฅ ์ธ๊ธฐ ์๋ ๋ฐฉ๋ฒ์ด์ผ. Python ZAP ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์นํด๋ณด์!
# Python ZAP ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น
pip install python-owasp-zap-v2.4
# ์ถ๊ฐ๋ก ์ ์ฉํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค
pip install requests
pip install python-dotenv # ํ๊ฒฝ๋ณ์ ๊ด๋ฆฌ์ฉ
Step 3: API ํค ์ค์
๋ณด์์ ์ํด ZAP API๋ API ํค๋ฅผ ์๊ตฌํด. ZAP์ ์ฒ์ ์คํํ๋ฉด ์๋์ผ๋ก ์์ฑ๋๋๋ฐ, ์ด๊ฑธ ํ์ธํ๊ณ ์ ์ฅํด๋์.
โ ๏ธ ๋ณด์ ์ฃผ์์ฌํญ
API ํค๋ ์ ๋ ์ฝ๋์ ํ๋์ฝ๋ฉํ์ง ๋ง! ํ๊ฒฝ๋ณ์๋ ๋ณ๋์ ์ค์ ํ์ผ๋ก ๊ด๋ฆฌํ๋ ๊ฒ ์ข์. Git์ ์ฌ๋ฆด ๋ ์ค์๋ก ๋ ธ์ถ๋๋ฉด ํฐ์ผ๋๊ฑฐ๋ . ๐
# .env ํ์ผ ์์
ZAP_API_KEY=your-secret-api-key-here
ZAP_HOST=localhost
ZAP_PORT=8080
TARGET_URL=https://example.com
๐ป ์ฒซ ๋ฒ์งธ ์๋ํ ์คํฌ๋ฆฝํธ ์์ฑํ๊ธฐ
๋๋์ด ์ค์ ์ฝ๋๋ฅผ ์์ฑํ ์๊ฐ์ด์ผ! ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ์ค์บ ์๋ํ ์คํฌ๋ฆฝํธ๋ถํฐ ์์ํด๋ณด์. ๐จ
๊ธฐ๋ณธ ์ค์บ ์คํฌ๋ฆฝํธ
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import time
import os
from zapv2 import ZAPv2
from dotenv import load_dotenv
# ํ๊ฒฝ๋ณ์ ๋ก๋
load_dotenv()
# ZAP ์ค์
ZAP_API_KEY = os.getenv('ZAP_API_KEY')
ZAP_HOST = os.getenv('ZAP_HOST', 'localhost')
ZAP_PORT = int(os.getenv('ZAP_PORT', 8080))
TARGET_URL = os.getenv('TARGET_URL')
# ZAP ์ธ์คํด์ค ์์ฑ
zap = ZAPv2(
apikey=ZAP_API_KEY,
proxies={
'http': f'http://{ZAP_HOST}:{ZAP_PORT}',
'https': f'http://{ZAP_HOST}:{ZAP_PORT}'
}
)
def main():
print(f"๐ ZAP ์๋ํ ์ค์บ ์์!")
print(f"๐ ๋์ URL: {TARGET_URL}")
# 1. ์ ์ธ์
์์
print("\nโจ ์ ์ธ์
์์ฑ ์ค...")
zap.core.new_session()
# 2. Spider ์คํ (์ฌ์ดํธ ํฌ๋กค๋ง)
print("\n๐ท๏ธ Spider ํฌ๋กค๋ง ์์...")
scan_id = zap.spider.scan(TARGET_URL)
# Spider ์งํ์ํฉ ๋ชจ๋ํฐ๋ง
while int(zap.spider.status(scan_id)) < 100:
progress = zap.spider.status(scan_id)
print(f"Spider ์งํ๋ฅ : {progress}%")
time.sleep(2)
print("โ
Spider ํฌ๋กค๋ง ์๋ฃ!")
# 3. Passive Scan ๋๊ธฐ
print("\n๐ Passive Scan ์งํ ์ค...")
while int(zap.pscan.records_to_scan) > 0:
records = zap.pscan.records_to_scan
print(f"๋จ์ ๋ ์ฝ๋: {records}")
time.sleep(2)
print("โ
Passive Scan ์๋ฃ!")
# 4. Active Scan ์คํ
print("\nโก Active Scan ์์...")
scan_id = zap.ascan.scan(TARGET_URL)
# Active Scan ์งํ์ํฉ ๋ชจ๋ํฐ๋ง
while int(zap.ascan.status(scan_id)) < 100:
progress = zap.ascan.status(scan_id)
print(f"Active Scan ์งํ๋ฅ : {progress}%")
time.sleep(5)
print("โ
Active Scan ์๋ฃ!")
# 5. ๊ฒฐ๊ณผ ์์ง
print("\n๐ ์ค์บ ๊ฒฐ๊ณผ ์์ง ์ค...")
alerts = zap.core.alerts(baseurl=TARGET_URL)
# ์ฌ๊ฐ๋๋ณ ๋ถ๋ฅ
high_alerts = [a for a in alerts if a['risk'] == 'High']
medium_alerts = [a for a in alerts if a['risk'] == 'Medium']
low_alerts = [a for a in alerts if a['risk'] == 'Low']
info_alerts = [a for a in alerts if a['risk'] == 'Informational']
# ๊ฒฐ๊ณผ ์ถ๋ ฅ
print("\n" + "="*50)
print("๐ฏ ์ค์บ ๊ฒฐ๊ณผ ์์ฝ")
print("="*50)
print(f"๐ด High: {len(high_alerts)}๊ฐ")
print(f"๐ก Medium: {len(medium_alerts)}๊ฐ")
print(f"๐ข Low: {len(low_alerts)}๊ฐ")
print(f"โน๏ธ Info: {len(info_alerts)}๊ฐ")
print("="*50)
# High ์๋ฆผ ์์ธ ์ถ๋ ฅ
if high_alerts:
print("\nโ ๏ธ ์ฌ๊ฐํ ์ทจ์ฝ์ ๋ฐ๊ฒฌ!")
for alert in high_alerts:
print(f"\n์ ๋ชฉ: {alert['alert']}")
print(f"URL: {alert['url']}")
print(f"์ค๋ช
: {alert['description'][:100]}...")
# 6. HTML ๋ฆฌํฌํธ ์์ฑ
print("\n๐ HTML ๋ฆฌํฌํธ ์์ฑ ์ค...")
html_report = zap.core.htmlreport()
report_filename = f"zap_report_{time.strftime('%Y%m%d_%H%M%S')}.html"
with open(report_filename, 'w', encoding='utf-8') as f:
f.write(html_report)
print(f"โ
๋ฆฌํฌํธ ์ ์ฅ ์๋ฃ: {report_filename}")
print("\n๐ ๋ชจ๋ ์์
์๋ฃ!")
if __name__ == "__main__":
main()
์ด ์คํฌ๋ฆฝํธ๋ ZAP ์๋ํ์ ๊ธฐ๋ณธ ํ๋ฆ์ ๋ณด์ฌ์ค. Spider๋ก ์ฌ์ดํธ๋ฅผ ํฌ๋กค๋งํ๊ณ , Passive Scan๊ณผ Active Scan์ ์์ฐจ์ ์ผ๋ก ์คํํ ๋ค์, ๊ฒฐ๊ณผ๋ฅผ ์์งํ๊ณ ๋ฆฌํฌํธ๋ฅผ ์์ฑํ๋ ๊ฑฐ์ง! ๐ฏ
๐ก Pro Tip
์ค์ ํ๋ก๋์ ํ๊ฒฝ์์๋ ํ์์์ ์ค์ , ์๋ฌ ํธ๋ค๋ง, ๋ก๊น ๋ฑ์ ์ถ๊ฐํด์ผ ํด. ์ ์ฝ๋๋ ํ์ต์ฉ ๊ธฐ๋ณธ ๋ฒ์ ์ด๋๊น ์ค์ ์ฌ์ฉํ ๋๋ ๋ ๊ฒฌ๊ณ ํ๊ฒ ๋ง๋ค์ด์ผ ํ๋ค๋ ์ ์์ง ๋ง!
๐ฏ ๊ณ ๊ธ ์๋ํ ๊ธฐ๋ฒ
๊ธฐ๋ณธ์ ๋ง์คํฐํ๋ค๋ฉด ์ด์ ์ข ๋ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค์ ์์๋ณผ ์ฐจ๋ก์ผ! ์ค๋ฌด์์ ์ ๋ง ์ ์ฉํ ํ ํฌ๋๋ค์ด๋๊น ์ง์คํด๋ด. ๐ฅ
1. ์ธ์ฆ์ด ํ์ํ ์ ํ๋ฆฌ์ผ์ด์ ํ ์คํธ
๋๋ถ๋ถ์ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ก๊ทธ์ธ์ด ํ์ํ์ง? ZAP์์ ์ธ์ฆ์ ์๋ํํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์!
def setup_authentication(zap, target_url, context_name):
"""
ํผ ๊ธฐ๋ฐ ์ธ์ฆ ์ค์
"""
print("๐ ์ธ์ฆ ์ค์ ์ค...")
# ์ปจํ
์คํธ ์์ฑ
context_id = zap.context.new_context(context_name)
# URL์ ์ปจํ
์คํธ์ ํฌํจ
zap.context.include_in_context(context_name, f"{target_url}.*")
# ๋ก๊ทธ์ธ ํ์ด์ง ์ค์
login_url = f"{target_url}/login"
zap.authentication.set_authentication_method(
contextid=context_id,
authmethodname='formBasedAuthentication',
authmethodconfigparams=f'loginUrl={login_url}&loginRequestData=username={{%username%}}&password={{%password%}}'
)
# ์ฌ์ฉ์ ์ถ๊ฐ
user_id = zap.users.new_user(context_id, 'test_user')
zap.users.set_authentication_credentials(
contextid=context_id,
userid=user_id,
authcredentialsconfigparams='username=testuser&password=testpass123'
)
# ์ฌ์ฉ์ ํ์ฑํ
zap.users.set_user_enabled(context_id, user_id, True)
print("โ
์ธ์ฆ ์ค์ ์๋ฃ!")
return context_id, user_id
2. ์ค์บ ์ ์ฑ ์ปค์คํฐ๋ง์ด์ง
๋ชจ๋ ์ทจ์ฝ์ ์ ๋ค ๊ฒ์ฌํ๋ฉด ์๊ฐ์ด ๋๋ฌด ์ค๋ ๊ฑธ๋ ค. ํ์ํ ๊ฒ๋ง ์ ํ์ ์ผ๋ก ์ค์บํ๋ ๋ฐฉ๋ฒ์ด์ผ!
def create_custom_scan_policy(zap, policy_name):
"""
์ปค์คํ
์ค์บ ์ ์ฑ
์์ฑ
"""
print(f"โ๏ธ ์ปค์คํ
์ค์บ ์ ์ฑ
'{policy_name}' ์์ฑ ์ค...")
# ๋ชจ๋ ์ค์บ๋ ๋นํ์ฑํ
scanners = zap.ascan.scanners()
for scanner in scanners:
zap.ascan.set_scanner_alert_threshold(
id=scanner['id'],
alertthreshold='OFF'
)
# ์ค์ํ ์ค์บ๋๋ง ํ์ฑํ
important_scanners = [
'40012', # Cross Site Scripting (Reflected)
'40014', # Cross Site Scripting (Persistent)
'40018', # SQL Injection
'90019', # Server Side Code Injection
'90020', # Remote OS Command Injection
]
for scanner_id in important_scanners:
zap.ascan.set_scanner_alert_threshold(
id=scanner_id,
alertthreshold='MEDIUM'
)
zap.ascan.set_scanner_attack_strength(
id=scanner_id,
attackstrength='HIGH'
)
print("โ
์ค์บ ์ ์ฑ
์ค์ ์๋ฃ!")
print(f"ํ์ฑํ๋ ์ค์บ๋: {len(important_scanners)}๊ฐ")
3. Ajax Spider ํ์ฉ
์์ฆ ์น์ฌ์ดํธ๋ ๋๋ถ๋ถ JavaScript๋ฅผ ๋ง์ด ์ฌ์ฉํ์์? ์ผ๋ฐ Spider๋ก๋ ์ฐพ์ง ๋ชปํ๋ ํ์ด์ง๋ค์ Ajax Spider๋ก ์ฐพ์ ์ ์์ด! ๐ธ๏ธ
def run_ajax_spider(zap, target_url):
"""
Ajax Spider ์คํ (JavaScript ๊ธฐ๋ฐ ์ ํ๋ฆฌ์ผ์ด์
์ฉ)
"""
print("๐ Ajax Spider ์์...")
# Ajax Spider ์ค์
zap.ajaxSpider.set_option_max_duration('10') # ์ต๋ 10๋ถ
zap.ajaxSpider.set_option_number_of_browsers('2') # ๋ธ๋ผ์ฐ์ 2๊ฐ ์ฌ์ฉ
# Ajax Spider ์คํ
zap.ajaxSpider.scan(target_url)
# ์งํ์ํฉ ๋ชจ๋ํฐ๋ง
while zap.ajaxSpider.status == 'running':
print(f"Ajax Spider ์คํ ์ค... (๋ฐ๊ฒฌ๋ URL: {zap.ajaxSpider.number_of_results})")
time.sleep(5)
print(f"โ
Ajax Spider ์๋ฃ! ์ด {zap.ajaxSpider.number_of_results}๊ฐ URL ๋ฐ๊ฒฌ")
# ๋ฐ๊ฒฌ๋ URL ๋ชฉ๋ก ๋ฐํ
return zap.ajaxSpider.results()
๐ CI/CD ํ์ดํ๋ผ์ธ ํตํฉ
์ง์ง ์๋ํ์ ํ์ CI/CD ํ์ดํ๋ผ์ธ์ ํตํฉํ ๋ ๋ฐํ๋ผ! ์ฝ๋๊ฐ ์ปค๋ฐ๋ ๋๋ง๋ค ์๋์ผ๋ก ๋ณด์ ํ ์คํธ๊ฐ ์คํ๋๋ ๊ฑฐ์ง. ๐
Jenkins ํตํฉ ์์
pipeline {
agent any
environment {
ZAP_HOST = 'localhost'
ZAP_PORT = '8080'
TARGET_URL = 'https://staging.example.com'
}
stages {
stage('Start ZAP') {
steps {
script {
// ZAP ๋ฐ๋ชฌ ์์
sh '''
docker run -d --name zap \
-p 8080:8080 \
-v $(pwd):/zap/wrk/:rw \
owasp/zap2docker-stable \
zap.sh -daemon -host 0.0.0.0 -port 8080 \
-config api.key=${ZAP_API_KEY}
'''
sleep(time: 30, unit: 'SECONDS')
}
}
}
stage('Run Security Scan') {
steps {
script {
sh 'python3 zap_automation.py'
}
}
}
stage('Generate Report') {
steps {
script {
// HTML ๋ฆฌํฌํธ ์์นด์ด๋ธ
archiveArtifacts artifacts: 'zap_report_*.html'
// ์ทจ์ฝ์ ์ด ๋ฐ๊ฒฌ๋๋ฉด ๋น๋ ์คํจ
sh '''
python3 check_vulnerabilities.py
'''
}
}
}
}
post {
always {
// ZAP ์ปจํ
์ด๋ ์ ๋ฆฌ
sh 'docker stop zap && docker rm zap'
}
failure {
// ์คํจ ์ ์๋ฆผ
emailext (
subject: "๋ณด์ ์ค์บ ์คํจ: ${env.JOB_NAME}",
body: "๋ณด์ ์ทจ์ฝ์ ์ด ๋ฐ๊ฒฌ๋์์ต๋๋ค. ๋ฆฌํฌํธ๋ฅผ ํ์ธํ์ธ์.",
to: "security-team@example.com"
)
}
}
}
GitHub Actions ํตํฉ ์์
name: Security Scan with ZAP
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
zap-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install dependencies
run: |
pip install python-owasp-zap-v2.4
pip install requests
- name: Run ZAP Docker
run: |
docker run -d --name zap \
-p 8080:8080 \
owasp/zap2docker-stable \
zap.sh -daemon -host 0.0.0.0 -port 8080 \
-config api.disablekey=true
sleep 30
- name: Run ZAP Scan
env:
TARGET_URL: ${{ secrets.TARGET_URL }}
run: python3 zap_automation.py
- name: Upload Report
uses: actions/upload-artifact@v2
with:
name: zap-report
path: zap_report_*.html
- name: Check Vulnerabilities
run: python3 check_vulnerabilities.py
- name: Stop ZAP
if: always()
run: docker stop zap && docker rm zap
๐ก CI/CD ํตํฉ ํ
๐ฏ ๋จ๊ณ์ ์ ๊ทผ: ์ฒ์์๋ ๊ฒฝ๊ณ ๋ง ํ๊ณ , ์ ์ฐจ ๋น๋ ์คํจ ์กฐ๊ฑด์ ๊ฐํํด๋๊ฐ
๐ฏ ๋ณ๋ ฌ ์คํ: ์ฌ๋ฌ ํ๊ฒฝ์ ๋์์ ํ
์คํธํ๋ฉด ์๊ฐ์ ์ ์ฝํ ์ ์์ด
๐ฏ ์บ์ฑ ํ์ฉ: ZAP ์ธ์
์ ์บ์ฑํ๋ฉด ๋ฐ๋ณต ์ค์บ์ด ๋นจ๋ผ์ ธ
๐ฏ ์๋ฆผ ์ค์ : Slack, Teams ๋ฑ์ผ๋ก ์ค์๊ฐ ์๋ฆผ์ ๋ฐ๋๋ก ์ค์ ํ์
๐ ๊ฒฐ๊ณผ ๋ถ์๊ณผ ๋ฆฌํฌํ
์ค์บ์ ์คํํ๋ ๊ฒ๋ ์ค์ํ์ง๋ง, ๊ฒฐ๊ณผ๋ฅผ ์ ๋๋ก ๋ถ์ํ๊ณ ๋ฆฌํฌํธํ๋ ๊ฒ๋ ์์ฒญ ์ค์ํด! ๐
๊ฒฐ๊ณผ ํํฐ๋ง๊ณผ ๋ถ๋ฅ
def analyze_scan_results(zap, target_url):
"""
์ค์บ ๊ฒฐ๊ณผ๋ฅผ ๋ถ์ํ๊ณ ๋ถ๋ฅ
"""
print("๐ ์ค์บ ๊ฒฐ๊ณผ ๋ถ์ ์ค...")
# ๋ชจ๋ ์๋ฆผ ๊ฐ์ ธ์ค๊ธฐ
alerts = zap.core.alerts(baseurl=target_url)
# ์ฌ๊ฐ๋๋ณ ๋ถ๋ฅ
severity_counts = {
'High': 0,
'Medium': 0,
'Low': 0,
'Informational': 0
}
# ์ทจ์ฝ์ ํ์
๋ณ ๋ถ๋ฅ
vulnerability_types = {}
for alert in alerts:
# ์ฌ๊ฐ๋ ์นด์ดํธ
severity_counts[alert['risk']] += 1
# ํ์
๋ณ ๋ถ๋ฅ
alert_name = alert['alert']
if alert_name not in vulnerability_types:
vulnerability_types[alert_name] = {
'count': 0,
'urls': [],
'risk': alert['risk']
}
vulnerability_types[alert_name]['count'] += 1
vulnerability_types[alert_name]['urls'].append(alert['url'])
# ๊ฒฐ๊ณผ ์ถ๋ ฅ
print("\n" + "="*60)
print("๐ ์ค์บ ๊ฒฐ๊ณผ ์์ธ ๋ถ์")
print("="*60)
print("\n๐ฏ ์ฌ๊ฐ๋๋ณ ํต๊ณ:")
for severity, count in severity_counts.items():
emoji = {'High': '๐ด', 'Medium': '๐ก', 'Low': '๐ข', 'Informational': 'โน๏ธ'}
print(f"{emoji[severity]} {severity}: {count}๊ฐ")
print("\n๐ ์ทจ์ฝ์ ํ์
๋ณ ํต๊ณ:")
sorted_vulns = sorted(
vulnerability_types.items(),
key=lambda x: x[1]['count'],
reverse=True
)
for vuln_name, vuln_data in sorted_vulns[:10]: # ์์ 10๊ฐ๋ง
print(f"\nโข {vuln_name}")
print(f" - ๋ฐ๊ฒฌ ํ์: {vuln_data['count']}ํ")
print(f" - ์ฌ๊ฐ๋: {vuln_data['risk']}")
print(f" - ์ํฅ๋ฐ๋ URL: {len(vuln_data['urls'])}๊ฐ")
return {
'severity_counts': severity_counts,
'vulnerability_types': vulnerability_types,
'total_alerts': len(alerts)
}
์ปค์คํ ๋ฆฌํฌํธ ์์ฑ
ZAP์ ๊ธฐ๋ณธ ๋ฆฌํฌํธ๋ ์ข์ง๋ง, ํ์ ๋ง๋ ์ปค์คํ ๋ฆฌํฌํธ๋ฅผ ๋ง๋ค๋ฉด ๋ ์ ์ฉํด!
import json
from datetime import datetime
def generate_custom_report(analysis_results, target_url, output_format='json'):
"""
์ปค์คํ
๋ฆฌํฌํธ ์์ฑ
"""
report_data = {
'scan_info': {
'target_url': target_url,
'scan_date': datetime.now().isoformat(),
'total_vulnerabilities': analysis_results['total_alerts']
},
'severity_summary': analysis_results['severity_counts'],
'vulnerability_details': analysis_results['vulnerability_types']
}
if output_format == 'json':
filename = f"custom_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
with open(filename, 'w', encoding='utf-8') as f:
json.dump(report_data, f, indent=2, ensure_ascii=False)
print(f"โ
JSON ๋ฆฌํฌํธ ์์ฑ: {filename}")
elif output_format == 'markdown':
filename = f"custom_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
with open(filename, 'w', encoding='utf-8') as f:
f.write(f"# ๋ณด์ ์ค์บ ๋ฆฌํฌํธ\n\n")
f.write(f"**๋์ URL:** {target_url}\n")
f.write(f"**์ค์บ ์ผ์:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
f.write("## ์ฌ๊ฐ๋๋ณ ์์ฝ\n\n")
for severity, count in report_data['severity_summary'].items():
f.write(f"- **{severity}:** {count}๊ฐ\n")
f.write("\n## ์ทจ์ฝ์ ์์ธ\n\n")
for vuln_name, vuln_data in report_data['vulnerability_details'].items():
f.write(f"### {vuln_name}\n")
f.write(f"- ์ฌ๊ฐ๋: {vuln_data['risk']}\n")
f.write(f"- ๋ฐ๊ฒฌ ํ์: {vuln_data['count']}ํ\n\n")
print(f"โ
Markdown ๋ฆฌํฌํธ ์์ฑ: {filename}")
return filename
๐จ ์ค์ ํ์ฉ ์๋๋ฆฌ์ค
์ด๋ก ์ ์ถฉ๋ถํ ๋ฐฐ์ ์ผ๋, ์ด์ ์ค์ ์ํฉ์์ ์ด๋ป๊ฒ ํ์ฉํ๋์ง ์์๋ณด์! ๐ผ
์๋๋ฆฌ์ค 1: ์ ๊ธฐ ๋ณด์ ์ ๊ฒ ์๋ํ
๋งค์ฃผ ์์์ผ ์ค์ 2์์ ์๋์ผ๋ก ๋ณด์ ์ค์บ์ ์คํํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ์ด๋ฉ์ผ๋ก ๋ฐ๋ ์์คํ ์ด์ผ.
import schedule
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
def weekly_security_scan():
"""
์ฃผ๊ฐ ์ ๊ธฐ ๋ณด์ ์ค์บ
"""
print("๐๏ธ ์ฃผ๊ฐ ์ ๊ธฐ ๋ณด์ ์ค์บ ์์...")
# ZAP ์ค์บ ์คํ
zap = initialize_zap()
run_full_scan(zap, TARGET_URL)
# ๊ฒฐ๊ณผ ๋ถ์
results = analyze_scan_results(zap, TARGET_URL)
# ๋ฆฌํฌํธ ์์ฑ
report_file = generate_custom_report(results, TARGET_URL, 'markdown')
# ์ด๋ฉ์ผ ๋ฐ์ก
send_email_report(
subject=f"์ฃผ๊ฐ ๋ณด์ ์ค์บ ๋ฆฌํฌํธ - {datetime.now().strftime('%Y-%m-%d')}",
body=f"์ด๋ฒ ์ฃผ ๋ณด์ ์ค์บ์ด ์๋ฃ๋์์ต๋๋ค.\n\n"
f"์ด {results['total_alerts']}๊ฐ์ ์ด์๊ฐ ๋ฐ๊ฒฌ๋์์ต๋๋ค.\n"
f"์์ธ ๋ด์ฉ์ ์ฒจ๋ถ ํ์ผ์ ํ์ธํ์ธ์.",
attachment=report_file
)
print("โ
์ฃผ๊ฐ ๋ณด์ ์ค์บ ์๋ฃ ๋ฐ ๋ฆฌํฌํธ ๋ฐ์ก!")
def send_email_report(subject, body, attachment):
"""
์ด๋ฉ์ผ๋ก ๋ฆฌํฌํธ ๋ฐ์ก
"""
msg = MIMEMultipart()
msg['From'] = 'security-bot@example.com'
msg['To'] = 'security-team@example.com'
msg['Subject'] = subject
msg.attach(MIMEText(body, 'plain'))
# ์ฒจ๋ถํ์ผ ์ถ๊ฐ
with open(attachment, 'rb') as f:
part = MIMEBase('application', 'octet-stream')
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header('Content-Disposition', f'attachment; filename={attachment}')
msg.attach(part)
# SMTP ์๋ฒ๋ก ์ ์ก
server = smtplib.SMTP('smtp.example.com', 587)
server.starttls()
server.login('security-bot@example.com', 'password')
server.send_message(msg)
server.quit()
# ์ค์ผ์ค ์ค์
schedule.every().monday.at("02:00").do(weekly_security_scan)
# ์ค์ผ์ค๋ฌ ์คํ
while True:
schedule.run_pending()
time.sleep(60)
์๋๋ฆฌ์ค 2: API ์๋ํฌ์ธํธ ๋ณด์ ํ ์คํธ
REST API์ ๊ฐ ์๋ํฌ์ธํธ๋ฅผ ์๋์ผ๋ก ํ ์คํธํ๋ ์คํฌ๋ฆฝํธ์ผ. API ๊ฐ๋ฐํ ๋ ์ ๋ง ์ ์ฉํด! ๐
def test_api_endpoints(zap, base_url, api_spec):
"""
OpenAPI/Swagger ์คํ ๊ธฐ๋ฐ API ํ
์คํธ
"""
print("๐ API ์๋ํฌ์ธํธ ๋ณด์ ํ
์คํธ ์์...")
# OpenAPI ์คํ ์ํฌํธ
zap.openapi.import_url(api_spec)
# ๊ฐ ์๋ํฌ์ธํธ๋ณ ํ
์คํธ
endpoints = [
{'method': 'GET', 'path': '/api/users'},
{'method': 'POST', 'path': '/api/users'},
{'method': 'GET', 'path': '/api/users/{id}'},
{'method': 'PUT', 'path': '/api/users/{id}'},
{'method': 'DELETE', 'path': '/api/users/{id}'},
]
results = {}
for endpoint in endpoints:
url = f"{base_url}{endpoint['path']}"
print(f"\nํ
์คํธ ์ค: {endpoint['method']} {url}")
# Active Scan ์คํ
scan_id = zap.ascan.scan(url)
# ์ค์บ ์๋ฃ ๋๊ธฐ
while int(zap.ascan.status(scan_id)) < 100:
time.sleep(2)
# ๊ฒฐ๊ณผ ์์ง
alerts = zap.core.alerts(baseurl=url)
results[f"{endpoint['method']} {endpoint['path']}"] = {
'alert_count': len(alerts),
'high_risk': len([a for a in alerts if a['risk'] == 'High']),
'alerts': alerts
}
print(f"โ
์๋ฃ: {len(alerts)}๊ฐ ์ด์ ๋ฐ๊ฒฌ")
# ๊ฒฐ๊ณผ ์์ฝ
print("\n" + "="*60)
print("๐ API ์๋ํฌ์ธํธ ํ
์คํธ ๊ฒฐ๊ณผ")
print("="*60)
for endpoint, data in results.items():
print(f"\n{endpoint}")
print(f" ์ด ์ด์: {data['alert_count']}๊ฐ")
print(f" High ์ํ: {data['high_risk']}๊ฐ")
return results
์๋๋ฆฌ์ค 3: ํ๊ท ํ ์คํธ (Regression Testing)
์ด์ ์ค์บ ๊ฒฐ๊ณผ์ ๋น๊ตํด์ ์๋ก์ด ์ทจ์ฝ์ ์ด ์๊ฒผ๋์ง ํ์ธํ๋ ๊ฑฐ์ผ!
def regression_test(zap, target_url, baseline_file):
"""
ํ๊ท ํ
์คํธ - ์ด์ ๊ฒฐ๊ณผ์ ๋น๊ต
"""
print("๐ ํ๊ท ํ
์คํธ ์์...")
# ํ์ฌ ์ค์บ ์คํ
current_results = run_full_scan(zap, target_url)
current_alerts = zap.core.alerts(baseurl=target_url)
# ๋ฒ ์ด์ค๋ผ์ธ ๋ก๋
with open(baseline_file, 'r') as f:
baseline_alerts = json.load(f)
# ์๋ก์ด ์ทจ์ฝ์ ์ฐพ๊ธฐ
new_vulnerabilities = []
fixed_vulnerabilities = []
current_alert_ids = {alert['alert'] + alert['url'] for alert in current_alerts}
baseline_alert_ids = {alert['alert'] + alert['url'] for alert in baseline_alerts}
# ์๋ก ๋ฐ๊ฒฌ๋ ์ทจ์ฝ์
for alert in current_alerts:
alert_id = alert['alert'] + alert['url']
if alert_id not in baseline_alert_ids:
new_vulnerabilities.append(alert)
# ์์ ๋ ์ทจ์ฝ์
for alert in baseline_alerts:
alert_id = alert['alert'] + alert['url']
if alert_id not in current_alert_ids:
fixed_vulnerabilities.append(alert)
# ๊ฒฐ๊ณผ ์ถ๋ ฅ
print("\n" + "="*60)
print("๐ ํ๊ท ํ
์คํธ ๊ฒฐ๊ณผ")
print("="*60)
print(f"โจ ์๋ก ๋ฐ๊ฒฌ๋ ์ทจ์ฝ์ : {len(new_vulnerabilities)}๊ฐ")
print(f"โ
์์ ๋ ์ทจ์ฝ์ : {len(fixed_vulnerabilities)}๊ฐ")
if new_vulnerabilities:
print("\nโ ๏ธ ์๋ก์ด ์ทจ์ฝ์ ์์ธ:")
for vuln in new_vulnerabilities:
print(f" โข {vuln['alert']} ({vuln['risk']})")
print(f" URL: {vuln['url']}")
if fixed_vulnerabilities:
print("\n๐ ์์ ๋ ์ทจ์ฝ์ :")
for vuln in fixed_vulnerabilities:
print(f" โข {vuln['alert']}")
# ํ์ฌ ๊ฒฐ๊ณผ๋ฅผ ์ ๋ฒ ์ด์ค๋ผ์ธ์ผ๋ก ์ ์ฅ
with open(baseline_file, 'w') as f:
json.dump(current_alerts, f, indent=2)
return {
'new': new_vulnerabilities,
'fixed': fixed_vulnerabilities
}
โก ์ฑ๋ฅ ์ต์ ํ ํ
ZAP ์ค์บ์ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆด ์ ์์ด. ํจ์จ์ ์ผ๋ก ๋ง๋๋ ๋ฐฉ๋ฒ๋ค์ ์์๋ณด์! โฑ๏ธ
๐ฏ ์ค์บ ๋ฒ์ ์ ํ
ํ์ํ ๋ถ๋ถ๋ง ์ค์บํ๋๋ก ์ปจํ ์คํธ๋ฅผ ์ ํํ ์ค์ ํด. ๋ถํ์ํ URL์ ์ ์ธํ์!
zap.context.exclude_from_context(
'MyContext',
'.*logout.*'
)
โ๏ธ ์ค๋ ๋ ์ ์กฐ์
์๋ฒ ์ฑ๋ฅ์ ๋ง์ถฐ ๋์ ์คํ ์ค๋ ๋ ์๋ฅผ ์กฐ์ ํ๋ฉด ์๋๊ฐ ๋นจ๋ผ์ ธ!
zap.ascan.set_option_thread_per_host(5)
zap.ascan.set_option_max_scan_duration_in_mins(30)
๐พ ์ธ์ ์ฌ์ฌ์ฉ
Spider ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅํด๋๊ณ ์ฌ์ฌ์ฉํ๋ฉด ์๊ฐ์ ์ ์ฝํ ์ ์์ด!
zap.core.save_session('my_session')
# ๋์ค์...
zap.core.load_session('my_session')
| ์ต์ ํ ๊ธฐ๋ฒ | ํจ๊ณผ | ์ ์ฉ ๋์ด๋ |
|---|---|---|
| ์ค์บ ์ ์ฑ ์ปค์คํฐ๋ง์ด์ง | 30-50% ์๊ฐ ๋จ์ถ | ์ฌ์ |
| ๋ณ๋ ฌ ์ค์บ ์คํ | 50-70% ์๊ฐ ๋จ์ถ | ์ค๊ฐ |
| ์ฆ๋ถ ์ค์บ (๋ณ๊ฒฝ๋ ๋ถ๋ถ๋ง) | 60-80% ์๊ฐ ๋จ์ถ | ์ด๋ ค์ |
| ์บ์ฑ ํ์ฉ | 20-40% ์๊ฐ ๋จ์ถ | ์ฌ์ |
| ์ค์บ ๋ฒ์ ์ต์ ํ | 40-60% ์๊ฐ ๋จ์ถ | ์ฌ์ |
๐ ๋ฌธ์ ํด๊ฒฐ ๊ฐ์ด๋
ZAP ์๋ํํ๋ค ๋ณด๋ฉด ์ฌ๋ฌ ๋ฌธ์ ๋ฅผ ๋ง๋ ์ ์์ด. ์์ฃผ ๋ฐ์ํ๋ ๋ฌธ์ ๋ค๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์ ๋ฆฌํด๋ดค์ด! ๐ง
๋ฌธ์ 1: ZAP API ์ฐ๊ฒฐ ์คํจ
์ฆ์: ConnectionError: Connection refused
์์ธ: ZAP์ด ์คํ๋์ง ์์๊ฑฐ๋ ํฌํธ๊ฐ ๋ค๋ฆ
ํด๊ฒฐ๋ฐฉ๋ฒ:
# ZAP์ด ์คํ ์ค์ธ์ง ํ์ธ
ps aux | grep zap
# ํฌํธ ํ์ธ
netstat -an | grep 8080
# ZAP ์ฌ์์
zap.sh -daemon -host 0.0.0.0 -port 8080
๋ฌธ์ 2: ์ค์บ์ด ๋ฉ์ถค
์ฆ์: ์ค์บ ์งํ๋ฅ ์ด 99%์์ ๋ฉ์ถค
์์ธ: ํ์์์ ์ค์ ์ด ๋๋ฌด ๊ธธ๊ฑฐ๋ ๋ฌดํ ๋ฃจํ
ํด๊ฒฐ๋ฐฉ๋ฒ:
# ํ์์์ ์ค์
zap.ascan.set_option_max_scan_duration_in_mins(60)
# ๊ฐ์ ์ค์ง ํ ์ฌ์์
zap.ascan.stop(scan_id)
time.sleep(5)
scan_id = zap.ascan.scan(target_url)
๋ฌธ์ 3: ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ
์ฆ์: OutOfMemoryError ๋๋ ์์คํ
๋๋ ค์ง
์์ธ: ํฐ ์ฌ์ดํธ ์ค์บ ์ ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ
ํด๊ฒฐ๋ฐฉ๋ฒ:
# ZAP ์คํ ์ ๋ฉ๋ชจ๋ฆฌ ํ ๋น
zap.sh -daemon -Xmx4g # 4GB ํ ๋น
# ๋๋ Docker ์ฌ์ฉ ์
docker run -m 4g owasp/zap2docker-stable
๋ฌธ์ 4: ์ธ์ฆ ์คํจ
์ฆ์: ๋ก๊ทธ์ธ ํ์ํ ํ์ด์ง๋ฅผ ์ค์บํ์ง ๋ชปํจ
์์ธ: ์ธ์ฆ ์ค์ ์ค๋ฅ ๋๋ ์ธ์ ๋ง๋ฃ
ํด๊ฒฐ๋ฐฉ๋ฒ:
# ์ธ์ฆ ๊ฒ์ฆ URL ์ค์
zap.authentication.set_logged_in_indicator(
contextid=context_id,
loggedinindicatorregex='Logout'
)
# ์ธ์
์ ์ง ํ์ธ
zap.httpsessions.set_active_session(
site=target_url,
session='session_name'
)
๐ ๋ฒ ์คํธ ํ๋ํฐ์ค
์ค๋ฌด์์ ZAP ์๋ํ๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ์ด์ํ๊ธฐ ์ํ ๋ฒ ์คํธ ํ๋ํฐ์ค๋ฅผ ๊ณต์ ํ ๊ฒ! ์ด๊ฑด ์ ๋ง ์ค์ํ ๋ด์ฉ์ด๋๊น ๊ผญ ๊ธฐ์ตํด๋ฌ. ๐
1๏ธโฃ ์ ์ง์ ๋์
์ฒ์๋ถํฐ ๋ชจ๋ ๊ฑธ ์๋ํํ๋ ค๊ณ ํ์ง ๋ง. ๋จผ์ ๊ฐ๋จํ ์ค์บ๋ถํฐ ์์ํด์ ์ ์ฐจ ํ์ฅํด๋๊ฐ๋ ๊ฒ ์ข์. ํ์ด ์ต์ํด์ง ์๊ฐ์ ์ค์ผ ํด!
2๏ธโฃ ํ๊ฒฝ๋ณ ์ค์ ๋ถ๋ฆฌ
๊ฐ๋ฐ, ์คํ ์ด์ง, ํ๋ก๋์ ํ๊ฒฝ๋ง๋ค ๋ค๋ฅธ ์ค์ ์ ์ฌ์ฉํด. ํ๋ก๋์ ์์๋ ๋ ์กฐ์ฌ์ค๋ฝ๊ฒ ์ค์บํด์ผ ํ๊ฑฐ๋ !
# config/dev.env
ZAP_ATTACK_STRENGTH=HIGH
ZAP_SCAN_POLICY=full
# config/prod.env
ZAP_ATTACK_STRENGTH=LOW
ZAP_SCAN_POLICY=passive_only
3๏ธโฃ ๋ก๊น ๊ณผ ๋ชจ๋ํฐ๋ง
๋ชจ๋ ์ค์บ ํ๋์ ๋ก๊ทธ๋ก ๋จ๊ธฐ๊ณ , ๋ฌธ์ ๊ฐ ์๊ธฐ๋ฉด ์ฆ์ ์๋ฆผ์ ๋ฐ๋๋ก ์ค์ ํด!
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('zap_automation.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
logger.info("ZAP ์ค์บ ์์")
4๏ธโฃ False Positive ๊ด๋ฆฌ
ZAP์ด ์๋ชป ํ์งํ๋ ๊ฒฝ์ฐ(False Positive)๊ฐ ์์ด. ์ด๋ฐ ๊ฑด ๋ฐ๋ก ๊ด๋ฆฌํด์ ๋ค์ ์ค์บ์์ ์ ์ธํ๋๋ก ํด!
# false_positives.json
{
"excluded_alerts": [
{
"alert": "X-Frame-Options Header Not Set",
"url": "https://example.com/public/*",
"reason": "Public pages don't need frame protection"
}
]
}
5๏ธโฃ ์ ๊ธฐ์ ์ธ ์ ๋ฐ์ดํธ
ZAP๊ณผ ํ๋ฌ๊ทธ์ธ์ ์ ๊ธฐ์ ์ผ๋ก ์ ๋ฐ์ดํธํด. ์๋ก์ด ์ทจ์ฝ์ ํจํด์ด ๊ณ์ ์ถ๊ฐ๋๊ฑฐ๋ !
# ์๋ ์
๋ฐ์ดํธ ์คํฌ๋ฆฝํธ
def update_zap():
print("๐ ZAP ์
๋ฐ์ดํธ ํ์ธ ์ค...")
zap.autoupdate.download_latest_release()
print("โ
์
๋ฐ์ดํธ ์๋ฃ!")
๐ ์ค์ ํ๋ก์ ํธ: ์์ ํ ์๋ํ ์์คํ ๊ตฌ์ถ
์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ชจ๋ ๊ฑธ ์ข ํฉํด์ ์ค์ ๋ก ์ฌ์ฉํ ์ ์๋ ์์ ํ ์๋ํ ์์คํ ์ ๋ง๋ค์ด๋ณด์! ์ด๊ฑด ์ง์ง ์ค๋ฌด์์ ๋ฐ๋ก ์ธ ์ ์๋ ์์ค์ด์ผ. ๐
ํ๋ก์ ํธ ๊ตฌ์กฐ
zap-automation/
โโโ config/
โ โโโ dev.env
โ โโโ staging.env
โ โโโ prod.env
โโโ scripts/
โ โโโ zap_scanner.py
โ โโโ report_generator.py
โ โโโ notification.py
โโโ policies/
โ โโโ full_scan.policy
โ โโโ quick_scan.policy
โโโ reports/
โ โโโ (generated reports)
โโโ logs/
โ โโโ (log files)
โโโ docker-compose.yml
โโโ requirements.txt
โโโ README.md
๋ฉ์ธ ์ค์บ๋ ๋ชจ๋ (zap_scanner.py)
#!/usr/bin/env python3
"""
ZAP ์๋ํ ์ค์บ๋ - ๋ฉ์ธ ๋ชจ๋
"""
import os
import sys
import time
import logging
from datetime import datetime
from zapv2 import ZAPv2
from dotenv import load_dotenv
import argparse
class ZAPScanner:
"""ZAP ์ค์บ๋ ํด๋์ค"""
def __init__(self, config_file=None):
"""์ด๊ธฐํ"""
if config_file:
load_dotenv(config_file)
else:
load_dotenv()
self.zap = ZAPv2(
apikey=os.getenv('ZAP_API_KEY'),
proxies={
'http': f"http://{os.getenv('ZAP_HOST')}:{os.getenv('ZAP_PORT')}",
'https': f"http://{os.getenv('ZAP_HOST')}:{os.getenv('ZAP_PORT')}"
}
)
self.target_url = os.getenv('TARGET_URL')
self.scan_policy = os.getenv('SCAN_POLICY', 'default')
# ๋ก๊น
์ค์
self.setup_logging()
def setup_logging(self):
"""๋ก๊น
์ค์ """
log_dir = 'logs'
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(
log_dir,
f"zap_scan_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_file),
logging.StreamHandler(sys.stdout)
]
)
self.logger = logging.getLogger(__name__)
self.logger.info("ZAP Scanner ์ด๊ธฐํ ์๋ฃ")
def check_zap_connection(self):
"""ZAP ์ฐ๊ฒฐ ํ์ธ"""
try:
version = self.zap.core.version
self.logger.info(f"ZAP ์ฐ๊ฒฐ ์ฑ๊ณต (๋ฒ์ : {version})")
return True
except Exception as e:
self.logger.error(f"ZAP ์ฐ๊ฒฐ ์คํจ: {e}")
return False
def setup_context(self, context_name="AutoScanContext"):
"""์ปจํ
์คํธ ์ค์ """
self.logger.info(f"์ปจํ
์คํธ '{context_name}' ์ค์ ์ค...")
# ์ ์ปจํ
์คํธ ์์ฑ
context_id = self.zap.context.new_context(context_name)
# URL ํฌํจ
self.zap.context.include_in_context(
context_name,
f"{self.target_url}.*"
)
# ์ ์ธํ URL ํจํด
exclude_patterns = [
'.*logout.*',
'.*signout.*',
'.*delete.*'
]
for pattern in exclude_patterns:
self.zap.context.exclude_from_context(context_name, pattern)
self.logger.info(f"์ปจํ
์คํธ ์ค์ ์๋ฃ (ID: {context_id})")
return context_id
def run_spider(self):
"""Spider ํฌ๋กค๋ง ์คํ"""
self.logger.info("Spider ํฌ๋กค๋ง ์์...")
scan_id = self.zap.spider.scan(self.target_url)
while int(self.zap.spider.status(scan_id)) < 100:
progress = self.zap.spider.status(scan_id)
self.logger.info(f"Spider ์งํ๋ฅ : {progress}%")
time.sleep(3)
urls_found = len(self.zap.spider.results(scan_id))
self.logger.info(f"Spider ์๋ฃ - {urls_found}๊ฐ URL ๋ฐ๊ฒฌ")
return urls_found
def run_ajax_spider(self):
"""Ajax Spider ์คํ"""
self.logger.info("Ajax Spider ์์...")
self.zap.ajaxSpider.set_option_max_duration('5')
self.zap.ajaxSpider.scan(self.target_url)
while self.zap.ajaxSpider.status == 'running':
results = self.zap.ajaxSpider.number_of_results
self.logger.info(f"Ajax Spider ์คํ ์ค... ({results}๊ฐ URL)")
time.sleep(5)
urls_found = self.zap.ajaxSpider.number_of_results
self.logger.info(f"Ajax Spider ์๋ฃ - {urls_found}๊ฐ URL ๋ฐ๊ฒฌ")
return urls_found
def wait_passive_scan(self):
"""Passive Scan ์๋ฃ ๋๊ธฐ"""
self.logger.info("Passive Scan ๋๊ธฐ ์ค...")
while int(self.zap.pscan.records_to_scan) > 0:
records = self.zap.pscan.records_to_scan
self.logger.info(f"Passive Scan ๋จ์ ๋ ์ฝ๋: {records}")
time.sleep(2)
self.logger.info("Passive Scan ์๋ฃ")
def run_active_scan(self):
"""Active Scan ์คํ"""
self.logger.info("Active Scan ์์...")
scan_id = self.zap.ascan.scan(self.target_url)
while int(self.zap.ascan.status(scan_id)) < 100:
progress = self.zap.ascan.status(scan_id)
self.logger.info(f"Active Scan ์งํ๋ฅ : {progress}%")
time.sleep(10)
self.logger.info("Active Scan ์๋ฃ")
return scan_id
def get_alerts(self):
"""์๋ฆผ ์์ง"""
self.logger.info("์ค์บ ๊ฒฐ๊ณผ ์์ง ์ค...")
alerts = self.zap.core.alerts(baseurl=self.target_url)
# ์ฌ๊ฐ๋๋ณ ๋ถ๋ฅ
severity_counts = {
'High': 0,
'Medium': 0,
'Low': 0,
'Informational': 0
}
for alert in alerts:
severity_counts[alert['risk']] += 1
self.logger.info("์ค์บ ๊ฒฐ๊ณผ ์์ฝ:")
for severity, count in severity_counts.items():
self.logger.info(f" {severity}: {count}๊ฐ")
return alerts, severity_counts
def generate_report(self, alerts):
"""๋ฆฌํฌํธ ์์ฑ"""
self.logger.info("๋ฆฌํฌํธ ์์ฑ ์ค...")
report_dir = 'reports'
os.makedirs(report_dir, exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# HTML ๋ฆฌํฌํธ
html_report = self.zap.core.htmlreport()
html_file = os.path.join(report_dir, f"zap_report_{timestamp}.html")
with open(html_file, 'w', encoding='utf-8') as f:
f.write(html_report)
self.logger.info(f"HTML ๋ฆฌํฌํธ ์์ฑ: {html_file}")
# JSON ๋ฆฌํฌํธ
import json
json_file = os.path.join(report_dir, f"zap_report_{timestamp}.json")
with open(json_file, 'w', encoding='utf-8') as f:
json.dump(alerts, f, indent=2, ensure_ascii=False)
self.logger.info(f"JSON ๋ฆฌํฌํธ ์์ฑ: {json_file}")
return html_file, json_file
def run_full_scan(self):
"""์ ์ฒด ์ค์บ ์คํ"""
self.logger.info("="*60)
self.logger.info("ZAP ์ ์ฒด ์ค์บ ์์")
self.logger.info(f"๋์ URL: {self.target_url}")
self.logger.info("="*60)
start_time = time.time()
try:
# 1. ZAP ์ฐ๊ฒฐ ํ์ธ
if not self.check_zap_connection():
raise Exception("ZAP ์ฐ๊ฒฐ ์คํจ")
# 2. ์ ์ธ์
์์
self.zap.core.new_session()
# 3. ์ปจํ
์คํธ ์ค์
context_id = self.setup_context()
# 4. Spider ์คํ
spider_urls = self.run_spider()
# 5. Ajax Spider ์คํ (์ต์
)
if os.getenv('USE_AJAX_SPIDER', 'false').lower() == 'true':
ajax_urls = self.run_ajax_spider()
# 6. Passive Scan ๋๊ธฐ
self.wait_passive_scan()
# 7. Active Scan ์คํ
scan_id = self.run_active_scan()
# 8. ๊ฒฐ๊ณผ ์์ง
alerts, severity_counts = self.get_alerts()
# 9. ๋ฆฌํฌํธ ์์ฑ
html_file, json_file = self.generate_report(alerts)
# 10. ์คํ ์๊ฐ ๊ณ์ฐ
elapsed_time = time.time() - start_time
self.logger.info("="*60)
self.logger.info("์ค์บ ์๋ฃ!")
self.logger.info(f"์ด ์คํ ์๊ฐ: {elapsed_time:.2f}์ด")
self.logger.info(f"๋ฐ๊ฒฌ๋ ์ด์: {len(alerts)}๊ฐ")
self.logger.info("="*60)
return {
'success': True,
'alerts': alerts,
'severity_counts': severity_counts,
'reports': {
'html': html_file,
'json': json_file
},
'elapsed_time': elapsed_time
}
except Exception as e:
self.logger.error(f"์ค์บ ์ค ์ค๋ฅ ๋ฐ์: {e}")
return {
'success': False,
'error': str(e)
}
def main():
"""๋ฉ์ธ ํจ์"""
parser = argparse.ArgumentParser(description='ZAP ์๋ํ ์ค์บ๋')
parser.add_argument(
'--config',
help='์ค์ ํ์ผ ๊ฒฝ๋ก',
default=None
)
parser.add_argument(
'--target',
help='๋์ URL',
default=None
)
args = parser.parse_args()
# ์ค์บ๋ ์ด๊ธฐํ
scanner = ZAPScanner(config_file=args.config)
# ๋์ URL ์ค๋ฒ๋ผ์ด๋
if args.target:
scanner.target_url = args.target
# ์ค์บ ์คํ
result = scanner.run_full_scan()
# ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ์ข
๋ฃ ์ฝ๋ ๋ฐํ
sys.exit(0 if result['success'] else 1)
if __name__ == "__main__":
main()
Docker Compose ์ค์
Docker๋ฅผ ์ฌ์ฉํ๋ฉด ํ๊ฒฝ ์ค์ ์ด ํจ์ฌ ์ฌ์์ ธ! ๐ณ
version: '3.8'
services:
zap:
image: owasp/zap2docker-stable
container_name: zap-daemon
ports:
- "8080:8080"
command: zap.sh -daemon -host 0.0.0.0 -port 8080 -config api.disablekey=true
volumes:
- ./reports:/zap/wrk
networks:
- zap-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080"]
interval: 30s
timeout: 10s
retries: 3
scanner:
build: .
container_name: zap-scanner
depends_on:
- zap
environment:
- ZAP_HOST=zap
- ZAP_PORT=8080
- TARGET_URL=${TARGET_URL}
volumes:
- ./reports:/app/reports
- ./logs:/app/logs
networks:
- zap-network
command: python zap_scanner.py
networks:
zap-network:
driver: bridge
๐ ์ฌ๋ฅ๋ท์์ ๋ณด์ ์ ๋ฌธ๊ฐ ์ฐพ๊ธฐ
ZAP ์๋ํ๋ฅผ ๋ฐฐ์ฐ๋ ๊ฑด ์ข์ง๋ง, ๋๋ก๋ ์ ๋ฌธ๊ฐ์ ๋์์ด ํ์ํ ๋๊ฐ ์์ด. ์ฌ๋ฅ๋ท์์๋ ๋ณด์ ํ ์คํ , ์๋ํ ์คํฌ๋ฆฝํ , DevSecOps ๊ตฌ์ถ ๋ฑ ๋ค์ํ ๋ณด์ ๊ด๋ จ ์ฌ๋ฅ์ ๊ฐ์ง ์ ๋ฌธ๊ฐ๋ค์ ๋ง๋ ์ ์์ด! ๐ผ
ํนํ ์ฒ์ ๋ณด์ ์๋ํ ์์คํ ์ ๊ตฌ์ถํ ๋๋ ๊ฒฝํ ์๋ ์ ๋ฌธ๊ฐ์ ์ปจ์คํ ์ ๋ฐ์ผ๋ฉด ์ํ์ฐฉ์ค๋ฅผ ๋ง์ด ์ค์ผ ์ ์๊ฑฐ๋ . ์ฌ๋ฅ๋ท์์ ๋ณด์ ์ ๋ฌธ๊ฐ๋ฅผ ์ฐพ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ! ๐ฏ
๐ ์ถ๊ฐ ํ์ต ๋ฆฌ์์ค
ZAP ์๋ํ๋ฅผ ๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด ์ด๋ฐ ๋ฆฌ์์ค๋ค์ ์ฐธ๊ณ ํด๋ด! ๐
๐ ๊ณต์ ๋ฌธ์
OWASP ZAP ๊ณต์ ๋ฌธ์๋ ๊ฐ์ฅ ์ ํํ๊ณ ์ต์ ์ ๋ณด๋ฅผ ์ ๊ณตํด.
www.zaproxy.org/docs
๐ฅ ๋น๋์ค ํํ ๋ฆฌ์ผ
YouTube์ ZAP ๊ด๋ จ ํํ ๋ฆฌ์ผ์ด ๋ง์. ํนํ OWASP ๊ณต์ ์ฑ๋์ ์ถ์ฒํด!
๐ฌ ์ปค๋ฎค๋ํฐ
ZAP User Group๊ณผ Stack Overflow์์ ๋ค๋ฅธ ์ฌ์ฉ์๋ค๊ณผ ๊ฒฝํ์ ๊ณต์ ํ ์ ์์ด.
๐ ๊ด๋ จ ์์
"Web Application Security Testing with ZAP" ๊ฐ์ ์ฑ ๋ค์ด ์ฒด๊ณ์ ์ธ ํ์ต์ ๋์์ด ๋ผ.
๐ฏ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ฝ์๋ค๋ฉด ์ ๋ง ๋๋จํด! ๐ ์ฐ๋ฆฌ๋ OWASP ZAP ์๋ํ ์คํฌ๋ฆฝํ ์ ๊ธฐ์ด๋ถํฐ ๊ณ ๊ธ ๊ธฐ๋ฒ, ์ค์ ํ๋ก์ ํธ๊น์ง ์ ๋ง ๋ง์ ๊ฑธ ๋ค๋ค์ด.
๋ณด์ ์๋ํ๋ ํ ๋ฒ ์ค์ ํด๋๋ฉด ๊ณ์ํด์ ๊ฐ์น๋ฅผ ์ ๊ณตํ๋ ํฌ์์ผ. ์ฒ์์๋ ์ค์ ํ๋ ๋ฐ ์๊ฐ์ด ๊ฑธ๋ฆฌ์ง๋ง, ์ผ๋จ ๊ตฌ์ถํ๊ณ ๋๋ฉด ๋ฐ๋ณต์ ์ธ ๋ณด์ ํ ์คํธ๋ฅผ ์๋์ผ๋ก ์ํํ ์ ์์ด์ ์์ฒญ๋ ์๊ฐ์ ์ ์ฝํ ์ ์์ง. โฐ
๐ ๋ค์ ๋จ๊ณ๋ก ๋์๊ฐ๊ธฐ
โจ ์์ ํ๋ก์ ํธ๋ถํฐ ์์ํด์ ์ ์ง์ ์ผ๋ก ํ์ฅํ๊ธฐ
โจ ํ์๋ค๊ณผ ์ง์ ๊ณต์ ํ๊ณ ํจ๊ป ์ฑ์ฅํ๊ธฐ
โจ ์ต์ ๋ณด์ ํธ๋ ๋ ๊ณ์ ํ์ตํ๊ธฐ
โจ ์ปค๋ฎค๋ํฐ์ ๊ธฐ์ฌํ๊ณ ๊ฒฝํ ๋๋๊ธฐ
โจ ์ค๋ฌด์ ์ ์ฉํ๋ฉด์ ์ง์์ ์ผ๋ก ๊ฐ์ ํ๊ธฐ
๋ณด์์ ํ ๋ฒ ํ๊ณ ๋๋๋ ๊ฒ ์๋๋ผ ์ง์์ ์ผ๋ก ๊ด๋ฆฌํด์ผ ํ๋ ํ๋ก์ธ์ค์ผ. ZAP ์๋ํ๋ ์ด๋ฐ ์ง์์ ์ธ ๋ณด์ ๊ด๋ฆฌ๋ฅผ ํจ์จ์ ์ผ๋ก ๋ง๋ค์ด์ฃผ๋ ๊ฐ๋ ฅํ ๋๊ตฌ์ง. ๐ช
์ด ๊ธ์ด ์ฌ๋ฌ๋ถ์ ๋ณด์ ์๋ํ ์ฌ์ ์ ๋์์ด ๋์์ผ๋ฉด ์ข๊ฒ ์ด. ๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋ ๋ ์๊ณ ์ถ์ ๋ด์ฉ์ด ์๋ค๋ฉด ์ธ์ ๋ ์ง ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํด๋ด. ์ฐ๋ฆฌ ๋ชจ๋ ํจ๊ป ๋ ์์ ํ ์น์ ๋ง๋ค์ด๊ฐ์! ๐
ํดํผ ํดํน! (๋ฌผ๋ก ํ์ดํธํ์ผ๋ก! ๐) ๐ก๏ธ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.