Semgrep으로 AI 생성 코드의 보안 패턴 자동 검사하기: 규칙 작성부터 PR 차단까지
하드코딩 비밀값·SQL 문자열 조립·검증 없는 리다이렉트 규칙 작성, 규칙 테스트, 예외 처리, GitHub Actions 연동

핵심 요약
- 레지스트리 규칙(--config auto)으로 일반 취약 패턴을, 팀 규칙으로 사내 안전 방식 우회를 잡는다.
- 규칙에는 id·message·severity·languages와 패턴 연산자 하나가 필수다.
- ruleid·ok 주석을 단 대상 파일을 두고 semgrep --test로 오탐·미탐을 함께 검증한다.
- 예외는 nosemgrep: 규칙ID와 이유·이슈 번호로 남기고 ID 없는 nosemgrep은 쓰지 않는다.
- CI에서는 --error로 PR을 막고 --baseline-commit으로 새로 생긴 결과만 본다.
AI 코딩 도구로 만든 코드는 동작은 하지만 보안상 위험한 지름길을 택하는 경우가 있다. 예제처럼 API 키를 코드에 직접 넣거나, 쿼리를 문자열로 이어 붙이거나, 요청 파라미터를 그대로 리다이렉트 주소로 쓰는 식이다. 리뷰어가 매번 눈으로 찾기에는 놓치기 쉽다. 이 글은 이런 패턴을 Semgrep 규칙으로 만들어 PR 단계에서 자동으로 막는 방법을 다룬다. 읽고 나면 팀 규칙 파일을 만들고, 규칙이 제대로 잡는지 테스트하고, CI에서 실패로 처리하는 데까지 진행할 수 있다.
레지스트리 규칙과 팀 규칙의 역할 나누기
Semgrep CI 예시의 기본 명령은 semgrep scan --config auto로, 공개 레지스트리 규칙을 자동 선택한다. 일반적인 취약 패턴은 여기서 걸러진다. 팀 규칙은 그 위에 "우리 코드베이스에서 이렇게 쓰면 안 된다"를 더하는 용도다. 예를 들어 사내 DB 헬퍼 대신 드라이버를 직접 호출하는 코드, 사내 인증 데코레이터가 빠진 라우트처럼 레지스트리가 알 수 없는 규칙이다.
도입 순서
- 레지스트리 규칙으로 현황 파악 — 로컬에서
semgrep scan --config auto를 실행해 기존 코드의 결과 규모를 본다. - 팀 규칙 3~5개 작성 — AI 생성 코드 리뷰에서 반복 지적된 패턴부터 규칙으로 만든다.
- 규칙 테스트 작성 — 규칙마다 걸려야 할 예와 걸리면 안 되는 예를 둔다.
- CI 연동 — 새로 생긴 결과만 PR을 막도록 기준 커밋을 지정한다.
- 예외 점검 — 한 달에 한 번
nosemgrep목록을 모아 여전히 필요한지 본다.
규칙 파일 구조
규칙 문법 문서에 따르면 규칙마다 id, message, severity, languages와 패턴 연산자 하나(pattern, patterns, pattern-either, pattern-regex 중 하나)가 필수다. 문서는 severity 값으로 LOW, MEDIUM, HIGH, CRITICAL을 안내한다. patterns는 AND, pattern-either는 OR, pattern-not은 제외, pattern-inside는 특정 범위 안으로 한정한다. $X 같은 메타변수는 임의의 식과 일치한다.
# .semgrep/rules/team-security.yaml
rules:
- id: hardcoded-api-key-assignment
languages: [python]
severity: HIGH
message: API 키를 코드에 직접 넣지 말고 환경 변수나 비밀 관리 도구에서 읽으세요.
patterns:
- pattern: $KEY = "$VALUE"
- metavariable-regex:
metavariable: $KEY
regex: (?i).*(api_key|secret|token).*
- id: sql-string-concat
languages: [python]
severity: HIGH
message: SQL을 문자열로 조립하지 말고 파라미터 바인딩을 쓰세요.
pattern-either:
- pattern: $CUR.execute("..." + $X)
- pattern: $CUR.execute(f"...")
- pattern: $CUR.execute("...".format(...))
- id: unvalidated-redirect
languages: [python]
severity: MEDIUM
message: 요청 값으로 리다이렉트할 때는 허용 목록으로 검증하세요.
patterns:
- pattern: redirect(request.args.get(...))
- pattern-not-inside: |
if is_safe_url(...):
...
첫 번째 규칙은 대입문 왼쪽 변수 이름이 api_key, secret, token을 포함하고 오른쪽이 문자열 리터럴일 때 걸린다. 세 번째 규칙의 is_safe_url은 각 팀의 검증 함수 이름으로 바꾼다. 이렇게 "팀이 정한 안전한 방법을 거치지 않은 코드"를 잡는 것이 팀 규칙의 핵심이다.
규칙이 제대로 잡는지 테스트하기
규칙은 오탐(잡지 말아야 할 것을 잡음)과 미탐(잡아야 할 것을 놓침)이 모두 생긴다. 규칙 테스트 문서에 따르면 규칙 파일과 같은 이름의 대상 파일을 두고, 걸려야 할 줄 바로 위에 ruleid: <규칙 id>, 걸리면 안 되는 줄 위에 ok: <규칙 id> 주석을 단다.
# .semgrep/rules/team-security.py
import os
# ruleid: hardcoded-api-key-assignment
OPENAI_API_KEY = "sk-test-1234"
# ok: hardcoded-api-key-assignment
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
def find(cur, name):
# ruleid: sql-string-concat
cur.execute(f"SELECT * FROM users WHERE name = '{name}'")
# ok: sql-string-concat
cur.execute("SELECT * FROM users WHERE name = %s", (name,))
semgrep --test .semgrep/rules/
규칙을 고칠 때마다 이 테스트를 먼저 돌린다. AI 도구에게 규칙 작성을 맡길 때도 "ruleid·ok 예시를 각각 두 개 이상 포함한 테스트 파일을 함께 만들고 semgrep --test가 통과하게 하라"고 요청하면 검증 가능한 결과물을 받을 수 있다.
예외를 관리하는 방법
정당한 예외는 무시 설정 문서의 nosemgrep 주석으로 처리한다. 규칙 ID를 함께 적으면 그 규칙만 무시한다. 주석은 해당 줄이나 바로 윗줄에 둔다.
# nosemgrep: sql-string-concat -- 테이블 이름은 내부 상수 목록에서만 선택됨 (#482)
cur.execute("SELECT count(*) FROM " + TABLES[kind])
ID 없는 nosemgrep은 그 줄의 모든 규칙을 끄므로 쓰지 않는 것이 좋다. 이유와 이슈 번호를 함께 적게 하면 나중에 예외 목록을 grep -rn nosemgrep으로 모아 점검할 수 있다. 파일 단위 제외는 .semgrepignore를 쓴다. 문서에 따르면 기본적으로 build/, dist/, node_modules/, tests/ 등이 제외되므로, 테스트 코드도 검사하려면 직접 .semgrepignore를 만들어 기본값을 바꿔야 한다.
GitHub Actions에서 PR 막기
CLI 레퍼런스에 따르면 --error를 주면 결과가 있을 때 종료 코드 1을 반환한다. 2 이상은 실행 자체의 실패(규칙 문법 오류 등)다. --baseline-commit을 주면 지정한 커밋에 이미 있던 결과는 빼고 새로 생긴 것만 보고하므로, 기존 코드에 쌓인 결과 때문에 도입 첫날부터 모든 PR이 막히는 일을 피할 수 있다.
# .github/workflows/semgrep.yml
name: semgrep
on: [pull_request]
jobs:
semgrep:
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- run: semgrep --test .semgrep/rules/
- run: |
semgrep scan \
--config auto \
--config .semgrep/rules/ \
--baseline-commit "$(git merge-base origin/main HEAD)" \
--error
규칙 테스트를 스캔보다 먼저 실행하면 규칙 자체가 깨졌을 때 바로 알 수 있다. 결과를 GitHub 코드 스캐닝 화면에서 보고 싶다면 --sarif-output으로 SARIF 파일을 만들어 업로드하는 방식도 있다.
다른 검사와의 위치 정리
| 단계 | 도구 | 잡는 것 |
|---|---|---|
| 커밋 전 | ESLint·Prettier·tsc | 문법·스타일·타입 |
| 커밋 전/PR | 시크릿 스캐너 | 실제 키 형태의 문자열 |
| PR | Semgrep 팀 규칙 | 팀이 정한 안전한 방법을 거치지 않은 코드 구조 |
| PR | 사람 리뷰 | 설계·권한·비즈니스 규칙 |
커밋 전 검사 구성은 ESLint·Prettier·tsc를 커밋 전에 자동 적용하기에, AI 도구가 비밀값 파일을 읽지 못하게 막는 설정은 시크릿·.env 차단 설정에, 바이브 코딩으로 만든 도구의 점검 항목은 사내 도구 보안 설정 점검에 정리했다.
초보자가 자주 실수하는 포인트
- 규칙 테스트 없이 규칙을 추가해 오탐으로 PR이 줄줄이 막히는 경우
- 규칙 ID 없이 nosemgrep을 달아 같은 줄의 다른 규칙까지 꺼지는 경우
- tests/ 폴더가 기본 제외라는 것을 모르고 테스트 코드도 검사된다고 생각하는 경우
- baseline 없이 도입해 기존 결과 때문에 모든 PR이 실패하는 경우
체크리스트
- 팀 규칙 파일마다 같은 이름의 테스트 대상 파일이 있는가
- semgrep --test가 CI에서 스캔보다 먼저 실행되는가
- nosemgrep 주석에 규칙 ID와 이유가 있는가
- --error와 --baseline-commit이 설정됐는가
- .semgrepignore 기본 제외 경로를 팀 의도에 맞게 확인했는가
자주 묻는 질문
자바스크립트·타입스크립트 코드에도 같은 규칙을 쓸 수 있나요?
규칙의 languages에 javascript나 typescript를 넣고 해당 언어 문법으로 패턴을 다시 써야 합니다. 언어마다 함수 호출 형태가 다르므로 언어별로 규칙을 나누고 각각 테스트 파일을 두는 편이 관리하기 쉽습니다.
자동 수정(fix)도 쓸 수 있나요?
규칙에 fix 키를 두면 단순한 치환 수정을 제안할 수 있고, .fixed 확장자 파일로 기대 결과를 테스트할 수 있습니다. 보안 규칙은 수정 방향이 상황마다 달라 자동 수정보다 메시지로 안내하는 경우가 많습니다.
참고 자료 · 검증 기준
- Semgrep Docs — Rule syntax
- Semgrep Docs — Testing rules
- Semgrep Docs — Ignore files, folders, and code
- Semgrep Docs — CLI reference
- Semgrep Docs — Sample CI configurations
위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.
이 글은 실제 실무 환경 테스트 및 검증된 기술 자료를 바탕으로 작성되었으며, 환경 변화에 따라 지속적으로 갱신됩니다.