블로그로 돌아가기

--ai-context와 --include-callee: AI 코드 리뷰의 hint이자 sink

raw 코드 대신 구조화된 보안 컨텍스트를 AI 소스코드 분석에 넘겨주는 v1의 두 플래그.

2025년 말쯤부터 AI가 개발 방식을 크게 바꿔놓았고, 소스코드 취약점 분석도 같은 방향으로 옮겨가고 있습니다.

AI 기반 소스코드 분석 도구 상당수가 코드를 LLM에 던지고 결과를 받는 패턴을 따릅니다. 그런데 이게 실제로 잘 동작하려면 LLM에게 "무엇을 봐야 하는지"를 알려주는 게 중요합니다. 그냥 코드베이스를 통째로 던지면 토큰만 태우고, 정작 봐야 할 지점은 놓치는 경우가 많거든요.

Noir v1에는 그 "무엇을 봐야 하는지"를 미리 추출해서 출력에 실어주는 두 플래그가 있습니다.

  • --include callee: 각 엔드포인트 핸들러의 1-hop 호출 그래프를 callees 배열로 추가
  • --ai-context: 같은 callee 정보를 포함해 5개 보안 카테고리로 분류하고 ai_context 아래에 추가

--include-callee: 누가 누구를 호출하나

둘 중 더 단순한 쪽입니다. 엔드포인트 핸들러 본문을 tree-sitter로 파싱해서 1-hop callee의 {name, path, line}을 뽑습니다.

$ noir -b ./flask_app --include callee -f json
{
  "url": "/sign",
  "method": "POST",
  "callees": [
    { "name": "get_hash",          "path": "app/utils.py", "line": 3 },
    { "name": "User.query.filter", "path": "app/app.py",   "line": 21 },
    { "name": "User",              "path": "app/app.py",   "line": 24 },
    { "name": "db_session.add",    "path": "app/app.py",   "line": 25 },
    { "name": "db_session.commit", "path": "app/app.py",   "line": 26 }
  ]
}

LLM 입장에서는 "이 엔드포인트를 보려면 utils.py:3과 app.py:21,24,25,26을 같이 봐야 한다"는 hint가 됩니다. 토큰 효율도 좋습니다. 코드베이스 전체를 던질 필요 없이 핸들러와 참조된 몇 줄만 있으면 되니까요.

--ai-context: 같은 데이터를 보안 카테고리로 분류

--ai-context는 같은 callee 정보를 가져다가 추가 분석을 거쳐 5가지 카테고리로 분류합니다.

  • guards: 인증 미들웨어 / 데코레이터 등 접근 제어
  • callees: 위 callee와 동일하지만 snippet과 confidence가 함께 붙음
  • sinks: SQL / command exec / file I/O / redirect / template render 같은 잠재적 sink
  • validators: 입력 검증 호출
  • signals: state_change, credential_input, guard_absence 같은 휴리스틱 신호

같은 Flask /sign POST 엔드포인트를 --ai-context로 돌리면

$ noir -b ./flask_app --ai-context -f json
{
  "url": "/sign",
  "method": "POST",
  "ai_context": {
    "sinks": [
      {
        "kind": "sql",
        "name": "query",
        "description": "Potential SQL/data-store sink inferred from code or callee name",
        "path": "app/app.py", "line": 21, "confidence": 78,
        "snippet": "20: password = get_hash(request.form['password'], ...) | 21: if User.query.filter(...).first(): | 22: return render_template('error.html')"
      }
    ],
    "signals": [
      { "kind": "state_change",     "name": "POST",          "confidence": 88 },
      { "kind": "credential_input", "name": "form.password", "confidence": 86 },
      { "kind": "guard_absence",    "name": "POST",          "confidence": 28,
        "description": "No auth guard was detected for this state-changing endpoint." }
    ]
  }
}

이 엔드포인트 하나로 LLM이 받는 정보를 정리하면

  1. credential_input이 있다 (password를 form으로 받음)
  2. state_change다 (POST)
  3. guard_absence다 (auth 데코레이터 안 보임)
  4. sql sink가 있다 (User.query.filter)

사람 리뷰어든 LLM이든 이 네 신호가 한곳에 모여 있다면 자연스럽게 "이 핸들러 먼저 보세요"라는 결론이 나옵니다. credential을 다루는 엔드포인트에 auth까지 없다면 더 따질 것도 없이 1순위죠.

반대로 auth가 잡힌 케이스도 봅니다 (flask_auth fixture 출력)

{
  "url": "/profile",
  "method": "GET",
  "ai_context": {
    "guards": [{
      "kind": "auth_guard",
      "name": "flask-login login_required",
      "description": "Protected by flask-login login_required",
      "confidence": 86,
      "snippet": "12: | 13: @login_required | 14: @app.route('/profile') | 15: def profile():"
    }]
  }
}

@login_required 데코레이터가 잡혀서 guards에 들어갔습니다. 이건 LLM에게 "이 엔드포인트는 auth가 걸려 있으니 다른 류의 취약점에 집중하라"는 negative signal로도 쓰입니다.

동시에 hint이자 sink

이 두 플래그를 동시에 hint이자 sink로 쓸 수 있다는 게 핵심입니다.

  • Hint로: 엔드포인트 단위로 컨텍스트를 미리 추려 LLM의 attention을 좁혀줍니다. "전체 코드베이스" 대신 "이 핸들러 + 이 파일들"만 보면 됩니다.
  • Sink로(source-to-sink의 그 sink): sinks 카테고리는 데이터 흐름의 종착점 후보를 framework-aware하게 알려줍니다. 일반 LLM도 User.query.filter가 SQL이라는 것 정도는 추론할 수 있지만, 매번 토큰을 태워야 합니다. Noir가 미리 라벨링해주면 그 추론 단계를 건너뛸 수 있습니다.

특히 framework-aware라는 점이 큽니다. 같은 query라는 callee 이름이라도 컨텍스트에 따라 의미가 다릅니다.

  • Flask + SQLAlchemy: User.query.filter → SQL sink
  • Express + MongoDB: User.find → NoSQL sink
  • 그냥 generic LLM은 이 차이를 모르거나 매번 추론을 다시 합니다

Noir는 detector 단계에서 framework를 식별하고, augmentor가 그 framework의 idiom에 맞춰 sink / guard 패턴을 적용합니다. LLM이 받는 건 raw 코드가 아니라 이미 framework-aware하게 정리된 context입니다.

권장 사용법

AI 기반 코드 리뷰 파이프라인을 짠다면

# 모든 카테고리
noir -b ./app --ai-context -f json

# 필요한 카테고리만 (token 절약)
noir -b ./app --ai-context=guards,sinks,signals -f json

# callee만 (가벼운 hint)
noir -b ./app --include callee -f json

전체 카테고리는 첫 audit 때 한 번 돌리는 정도가 적당합니다. 이후 incremental review나 PR 단위에서는 guards,sinks,signals만으로도 충분한 경우가 많습니다.

다음 계획

--ai-context의 sink / guard 패턴은 현재 정규식 기반 휴리스틱입니다. 데이터 흐름 자체를 따라가지는 않고, callee 이름과 코드 패턴으로 "여기 sink 같은데" 정도까지만 표시합니다. 이건 의도된 trade-off예요. 정확한 taint analysis는 전용 도구가 더 잘하고, Noir는 그 도구나 LLM에게 focus point를 빠르게 던지는 역할에 충실하려고 합니다.

앞으로는 패턴 카탈로그를 더 framework-aware하게 확장하고 signal 종류도 늘릴 계획입니다. 피드백이나 새 패턴 제안은 언제나 환영합니다.