Post

NotebookLM + Gemini API로 소스코드 취약점 진단을 자동화하는 방법

NotebookLM + Gemini API로 소스코드 취약점 진단을 자동화하는 방법

왜 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 25CWE 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 티켓까지 자동 생성되는 환경을 만들 수 있습니다.

This post is licensed under CC BY 4.0 by the author.

© Pedro. Some rights reserved.