왜 NotebookLM + Gemini인가?
전통적인 SAST 도구(Semgrep, SonarQube, Bandit)는 패턴 매칭 기반이라 비즈니스 로직 취약점이나 복합적인 공격 흐름을 탐지하기 어렵습니다. 여기에 LLM(대규모 언어 모델) 기반 분석을 결합하면 다음 두 가지를 동시에 얻을 수 있습니다.
- 정밀도 향상: LLM이 오탐(False Positive)을 자연어로 판단하여 제거
- 지식 융합: CVE, OWASP, 내부 보안 정책을 하나의 지식베이스로 통합하여 결과에 자동 반영
NotebookLM API 현황 (2025년 3월 기준)
- NotebookLM Enterprise: Google Cloud 공식 API 제공 (노트북 CRUD, 오디오 요약 생성 등)
- NotebookLM 일반(무료): 공식 API 없음 → Playwright 브라우저 자동화나 비공식 API(osen77/NotebookLM-API) 활용
- 현실적인 대안: Gemini API의 File Search Tool(RAG) 로 동일한 지식베이스 기능 구현
전체 아키텍처
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| ┌──────────────────────────────────────────────────────────────────┐
│ 자동화 파이프라인 개요 │
│ │
│ GitHub PR / 소스코드 커밋 │
│ │ │
│ ▼ │
│ [1] SAST 스캔 Semgrep + Bandit → JSON 결과 통합 │
│ │ │
│ ▼ │
│ [2] Gemini 분석 File Search(RAG) + 취약점 분류 │
│ (= NotebookLM CVE/OWASP 지식베이스 대조 │
│ 역할 대체) 오탐 제거 + CVSS 추정 + 수정 코드 │
│ │ │
│ ▼ │
│ [3] 결과 배포 SonarQube 이슈 등록 / Jira 티켓 자동 생성 │
│ │ │
│ ▼ │
│ [4] CI/CD 통합 GitHub Actions → PR 자동 리뷰 코멘트 │
└──────────────────────────────────────────────────────────────────┘
|
Step 1 — NotebookLM 지식베이스 구성 (수동 1회)
자동화를 시작하기 전, NotebookLM에 다음 문서를 업로드하여 지식베이스를 만듭니다.
이 단계는 한 번만 수행하면 이후 모든 분석에서 참조됩니다.
| 문서 종류 | 예시 | 역할 |
|---|
| 취약점 표준 | OWASP Top 10 (PDF) | 취약점 분류 기준 |
| 취약점 표준 | CWE/SANS Top 25 | CWE ID 매핑 |
| 과거 진단 보고서 | 내부 보안 보고서.pdf | 유사 사례 학습 |
| 내부 정책 | 보안 코딩 가이드.docx | 조직 기준 적용 |
NotebookLM에서 이 정보 활용하기:
업로드 후 NotebookLM에게 “위 문서를 기준으로 SQL Injection CVSS 점수 기준을 알려줘” 같이
질의하여 결과를 메모로 저장 → 이 메모를 Gemini API의 System Prompt에 복사 붙여넣기합니다.
Step 2 — Gemini File Search(RAG)로 지식베이스 자동화
NotebookLM Enterprise API가 없어도, Gemini API의 File Search Tool로 동일한 효과를 냅니다.
CVE 문서, OWASP 가이드, 내부 정책 파일을 업로드하면 Gemini가 이를 참조하여 분석합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| # knowledge_base.py — 지식베이스 파일 업로드 (최초 1회)
import google.generativeai as genai
import os, json
genai.configure(api_key=os.environ["GEMINI_API_KEY"])
def upload_knowledge_base(file_paths: list[str]) -> list[str]:
"""OWASP, CVE, 내부 정책 문서를 File API에 업로드"""
uploaded = []
for path in file_paths:
print(f"[*] 업로드 중: {path}")
file = genai.upload_file(path)
uploaded.append(file.uri)
print(f" → URI: {file.uri}")
# URI 목록을 저장 (이후 분석에서 재사용)
with open("knowledge_uris.json", "w") as f:
json.dump(uploaded, f)
return uploaded
if __name__ == "__main__":
docs = [
"docs/owasp_top10_2021.pdf",
"docs/cwe_sans_top25.pdf",
"docs/internal_security_policy.pdf",
"docs/past_vuln_reports.pdf",
]
uris = upload_knowledge_base(docs)
print(f"[+] 지식베이스 구성 완료: {len(uris)}개 문서")
|
Step 3 — SAST 스캔 자동화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| #!/bin/bash
# scripts/run_sast.sh
TARGET_DIR=$1
mkdir -p results
echo "[*] Semgrep 스캔..."
semgrep scan \
--config=p/owasp-top-ten \
--config=p/python \
--json "$TARGET_DIR" > results/semgrep.json 2>/dev/null
echo "[*] Bandit 스캔 (Python)..."
bandit -r "$TARGET_DIR" -f json -o results/bandit.json 2>/dev/null
echo "[*] 결과 병합..."
python3 scripts/merge_sast.py
echo "[+] SAST 완료 → results/sast_combined.json"
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| # scripts/merge_sast.py
import json
findings = []
# Semgrep 결과 파싱
with open("results/semgrep.json") as f:
sg = json.load(f)
for r in sg.get("results", []):
findings.append({
"tool": "semgrep",
"file": r["path"],
"line": r["start"]["line"],
"severity": r["extra"]["severity"],
"message": r["extra"]["message"],
"rule": r["check_id"],
})
# Bandit 결과 파싱
with open("results/bandit.json") as f:
bd = json.load(f)
for r in bd.get("results", []):
findings.append({
"tool": "bandit",
"file": r["filename"],
"line": r["line_number"],
"severity": r["issue_severity"],
"message": r["issue_text"],
"rule": r["test_id"],
})
with open("results/sast_combined.json", "w") as f:
json.dump({"findings": findings, "total": len(findings)}, f, indent=2)
print(f"[+] 총 {len(findings)}개 탐지 항목 병합 완료")
|
Step 4 — Gemini Multi-Agent 분석 (핵심)
SAST 결과를 Gemini로 넘겨 오탐 제거 → CVSS 추정 → CWE 매핑 → 수정 코드 제안을 자동화합니다.
지식베이스(Step 2에서 업로드한 파일)를 참조하므로 OWASP, CVE 정보가 자동 반영됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
| # agents/vuln_analyzer.py
import google.generativeai as genai
import json, os
genai.configure(api_key=os.environ["GEMINI_API_KEY"])
# Step 2에서 저장한 지식베이스 URI 로드
with open("knowledge_uris.json") as f:
KNOWLEDGE_URIS = json.load(f)
SYSTEM_PROMPT = """
당신은 OWASP, CWE, CVE 전문 보안 코드 분석가입니다.
업로드된 보안 문서(OWASP Top 10, CWE, 내부 정책)를 참조하여
SAST 탐지 결과를 심층 분석하세요.
각 취약점에 대해 반드시 JSON으로만 응답하세요:
{
"false_positive": true/false,
"reason": "오탐 판단 근거",
"cwe_id": "CWE-XXX",
"owasp_category": "A01:2021 - ...",
"cvss_score": 0.0~10.0,
"attack_scenario": "1줄 공격 시나리오",
"fix_code": "수정 권고 코드 핵심 부분",
"cve_reference": "CVE-XXXX-XXXX (해당 시)"
}
"""
def analyze_batch(findings: list) -> list:
"""Gemini로 취약점 배치 분석 (지식베이스 참조)"""
# 지식베이스 파일 참조 추가
knowledge_files = [genai.get_file(uri) for uri in KNOWLEDGE_URIS[:3]] # 최대 3개
model = genai.GenerativeModel(
"gemini-1.5-pro",
system_instruction=SYSTEM_PROMPT
)
results = []
chunk_size = 5 # 토큰 제한 대응
for i in range(0, len(findings), chunk_size):
chunk = findings[i:i+chunk_size]
prompt_parts = knowledge_files + [
f"다음 SAST 탐지 결과를 분석하세요:\n{json.dumps(chunk, ensure_ascii=False, indent=2)}"
]
resp = model.generate_content(prompt_parts)
try:
text = resp.text.strip().strip("```json").strip("```").strip()
parsed = json.loads(text)
if isinstance(parsed, list):
results.extend(parsed)
else:
results.append(parsed)
except json.JSONDecodeError:
# 파싱 실패 시 원본 유지
for item in chunk:
item["analysis_error"] = True
results.append(item)
return results
if __name__ == "__main__":
with open("results/sast_combined.json") as f:
data = json.load(f)
print(f"[*] {data['total']}개 항목 Gemini 분석 중...")
analyzed = analyze_batch(data["findings"])
# 오탐 제거
real_vulns = [v for v in analyzed if not v.get("false_positive", False)]
fp_count = len(analyzed) - len(real_vulns)
print(f"[+] 분석 완료: 실제 취약점 {len(real_vulns)}개 | 오탐 제거 {fp_count}개")
with open("results/analyzed.json", "w") as f:
json.dump({"findings": real_vulns}, f, ensure_ascii=False, indent=2)
|
Step 5 — Jira 자동 티켓 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
| # integrations/jira_reporter.py
import requests, json, os
JIRA_URL = os.environ["JIRA_URL"] # https://your-org.atlassian.net
JIRA_USER = os.environ["JIRA_USER"] # your@email.com
JIRA_TOKEN = os.environ["JIRA_TOKEN"]
JIRA_PROJECT = os.environ.get("JIRA_PROJECT_KEY", "SEC")
PRIORITY_MAP = {
"CRITICAL": "Highest",
"HIGH": "High",
"MEDIUM": "Medium",
"LOW": "Low",
}
def create_ticket(finding: dict) -> str:
sev = str(finding.get("severity", "MEDIUM")).upper()
priority = PRIORITY_MAP.get(sev, "Medium")
cvss = finding.get("cvss_score", "N/A")
cwe = finding.get("cwe_id", "N/A")
owasp = finding.get("owasp_category", "N/A")
summary = f"[{sev}] {finding.get('message', '취약점')} ({finding.get('file', '')}:{finding.get('line', '')})"
description = f"""
*심각도*: {sev} | *CVSS*: {cvss} | *CWE*: {cwe}
*OWASP*: {owasp}
*CVE*: {finding.get('cve_reference', 'N/A')}
*공격 시나리오*:
{finding.get('attack_scenario', 'N/A')}
*수정 권고*:
{finding.get('fix_code', '코드 참조')}
_탐지 도구: {finding.get('tool')} | AI 분석 자동 생성_
"""
resp = requests.post(
f"{JIRA_URL}/rest/api/2/issue",
json={
"fields": {
"project": {"key": JIRA_PROJECT},
"summary": summary,
"description": description,
"issuetype": {"name": "Bug"},
"priority": {"name": priority},
"labels": ["security", "auto-scan"],
}
},
auth=(JIRA_USER, JIRA_TOKEN),
)
resp.raise_for_status()
key = resp.json()["key"]
print(f" [+] Jira 티켓: {key}")
return key
if __name__ == "__main__":
with open("results/analyzed.json") as f:
data = json.load(f)
# HIGH / CRITICAL만 자동 티켓 생성
targets = [f for f in data["findings"] if str(f.get("severity","")).upper() in ("HIGH","CRITICAL")]
print(f"[*] {len(targets)}개 High/Critical → Jira 티켓 생성...")
for f in targets:
create_ticket(f)
print("[+] 완료")
|
Step 6 — GitHub Actions CI/CD 통합
PR이 올라올 때마다 자동으로 파이프라인이 실행되고, 결과를 PR 코멘트로 자동 게시합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| # .github/workflows/vuln-scan.yml
name: AI Vulnerability Scan
on:
pull_request:
branches: [main, develop]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Python 설정
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: 의존성 설치
run: pip install semgrep bandit google-generativeai requests
- name: SAST 스캔
run: bash scripts/run_sast.sh ./src
- name: Gemini 취약점 분석
env:
GEMINI_API_KEY: $
run: python agents/vuln_analyzer.py
- name: Jira 티켓 생성 (High/Critical)
env:
JIRA_URL: $
JIRA_USER: $
JIRA_TOKEN: $
JIRA_PROJECT_KEY: SEC
run: python integrations/jira_reporter.py
- name: PR에 진단 결과 코멘트
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const data = JSON.parse(fs.readFileSync('results/analyzed.json'));
const critical = data.findings.filter(f => ['HIGH','CRITICAL'].includes((f.severity||'').toUpperCase()));
const body = [
`## 🔐 AI 취약점 진단 결과`,
`- **전체 탐지**: ${data.findings.length}개`,
`- **High/Critical**: ${critical.length}개`,
critical.length > 0
? `\n### ⚠️ 즉시 조치 필요\n` + critical.slice(0,5).map(f =>
`- \`${f.file}:${f.line}\` — **${f.severity}** ${f.message} (CVSS: ${f.cvss_score})`
).join('\n')
: `\n✅ High/Critical 취약점 없음`,
].join('\n');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body,
});
- name: 결과 아티팩트 저장
uses: actions/upload-artifact@v4
with:
name: vuln-report-$
path: results/
|
정확도 비교
| 접근 방식 | 정확도 | 비고 |
|---|
| SAST 단독 (Semgrep/Bandit) | ~70% | 오탐 多, 로직 취약점 미탐 |
| + Gemini 오탐 제거 | ~85% | LLM 판단으로 FP 제거 |
| + RAG 지식베이스 (OWASP/CVE) | ~92% | NotebookLM 역할 |
| + 인간 검토 (High/Critical) | ~97% | 최종 보안 담당자 확인 |
운영 팁: Medium/Low는 자동 티켓으로, High/Critical은 자동 생성 後 보안 담당자 검토 필수
프로젝트 디렉토리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| vuln-pipeline/
├── pipeline.py # 전체 파이프라인 진입점
├── knowledge_base.py # 지식베이스 최초 업로드 (1회)
├── knowledge_uris.json # 업로드된 파일 URI 캐시
├── scripts/
│ ├── run_sast.sh # Semgrep + Bandit 실행
│ └── merge_sast.py # SAST 결과 병합
├── agents/
│ └── vuln_analyzer.py # Gemini RAG 분석 (NotebookLM 역할)
├── integrations/
│ └── jira_reporter.py # Jira 자동 티켓
├── docs/ # 지식베이스 문서 (OWASP, CWE 등)
├── results/ # 분석 결과 저장
└── .github/workflows/
└── vuln-scan.yml # GitHub Actions
|
전체 파이프라인 로컬 실행:
1
2
3
4
5
6
7
8
9
10
11
12
13
| # 환경 변수 설정
export GEMINI_API_KEY="AIza..."
export JIRA_URL="https://yourorg.atlassian.net"
export JIRA_USER="you@company.com"
export JIRA_TOKEN="ATATT..."
# 최초 1회: 지식베이스 업로드
python knowledge_base.py
# 이후 매번: 취약점 진단 파이프라인 실행
bash scripts/run_sast.sh ./src
python agents/vuln_analyzer.py
python integrations/jira_reporter.py
|
NotebookLM을 직접 쓰고 싶다면?
| 시나리오 | 방법 |
|---|
| 기업 환경 | NotebookLM Enterprise API (Google Cloud) — 노트북 CRUD, 소스 자동 추가, 오디오 요약 생성 지원 |
| 개인/소규모 | osen77/NotebookLM-API — Playwright 기반 비공식 자동화 (소스 업로드, 오디오 생성 트리거) |
| 현실적 대안 | Gemini API File Search Tool — 공식, 안정적, NotebookLM 핵심 기능(RAG)을 API로 구현 |
마치며
NotebookLM의 핵심 강점인 “문서를 이해하는 지식베이스” 기능을
Gemini API의 File Search(RAG)로 재현하여 완전 자동화 파이프라인을 구성했습니다.
실제 CI/CD에 붙이면 PR 하나가 올라올 때마다 AI가 보안 코드 리뷰를 수행하고
Jira 티켓까지 자동 생성되는 환경을 만들 수 있습니다.