작성했는데, 토큰 사용량이 너무 많고 검색이 느려서 아직 본격적인 프로덕션 환경에는 무리지만, 만드는 동안 고민 자체로 유의미한 시도였다. 다음에 시간될때 좀 더 다듬어볼 예정이다.
기존 AWS 보안 진단 도구들(Prowler, ScoutSuite, CloudSploit 등)은 룰 기반이다. 특정 설정 패턴을 매칭해서 "이 규칙을 위반했다"라고 알려주는 식이다. 잘 만들어져 있고 빠르지만, 두 가지가 아쉬웠다:
LLM에 자원 구성을 던져주고 취약점을 분석시키면 이 두 가지를 메울 수 있지 않을까 싶어서 만들어봤다. Amazon Bedrock + Claude Sonnet 4.6 조합으로.
최종적으로 만든 구조는 이렇다.
┌─────────────────────────────────────────────────────────┐
│ CLI (Typer) FastAPI │
└────────────┬──────────────────────┬─────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ Scanner │
│ 1. SessionFactory (single / assume-role org) │
│ 2. Collectors (19 services, per-region) │
│ 3. ResourceAnalyzer (Bedrock concurrent batches) │
│ 4. AttackChainAnalyzer (cross-resource correlation) │
│ 5. Exporters (CSV / JSON / HTML / S3) │
└─────────────────────────────────────────────────────────┘
│
├──▶ Amazon Bedrock (Claude Sonnet 4.6)
└──▶ AWS APIs (boto3)
핵심 아이디어:
처음엔 자원 하나당 요청 하나를 생각했다. 계산해보니 AWS Organization 규모면 수천 건이라 말이 안 된다. 배치로 묶었다. 서비스별로 그룹핑해서 5~8개씩 한 프롬프트에 넣고, 표준(CIS, NIST 등)에 대한 분석을 한 번에 요청한다.
문제는 IAM role처럼 설정이 큰 자원이다. 배치 크기 20으로 넣었더니 ValidationException이 터졌다. IAM role 하나에 trust policy + inline policy + attached policy 전부 담으면 수십 KB다. 결국 배치 크기를 5로 낮췄다.
처음엔 각 리전, 각 서비스를 순회하면서 수집했다. 17개 리전 × 19개 서비스 = 수백 번의 API 호출. 느리다.
그러다 Amazon Resource Explorer로 자원을 수집하는 방향으로 방향을 변경해봤다. aggregator 인덱스를 설정해두면 쿼리 한 번으로 모든 리전의 모든 자원을 받아볼 수 있다. 테스트 계정에서 써봤더니 1회 호출로 1000개 자원이 바로 나왔다.
실제 도구에서는 Resource Explorer로 전체 인벤토리를 먼저 파악하고, 그걸 기준으로 상세 설정을 수집하는 하이브리드 방식이 되어야 한다. 이건 다음 버전 숙제.
배치 내부는 asyncio.gather로 동시 실행되는데, 스크립트에서 서비스별로 await analyzer.analyze(iam_resources) → await analyzer.analyze(s3_resources) 식으로 순차 호출하고 있었다. IAM(339개)이 끝날 때까지 S3가 기다리는 구조.
수정한 게:
gather결과적으로 IO 대기는 semaphore가 허용하는 만큼 동시에 깔린다. 이론적으론 10배 빨라져야 한다.
이게 진짜 복병이었다. 분석 중간에 ThrottlingException: Too many tokens per day가 나오기 시작했다. 확인해보니 Bedrock은 리전별로 일일 토큰 쿼터가 있다. us-east-1에서 며칠간 여러 번 테스트하면서 쿼터를 다 써버렸다.
우회책:
--exclude-resource-types iam:role CLI 옵션으로 특정 리소스 타입 제외 가능근본 해결은 아니다. 프롬프트 자체를 줄이거나 요약해서 보내는 전처리가 필요하다. 현재는 설정 JSON을 거의 raw로 넘기고 있어서 토큰이 많이 든다.
Claude에게 "JSON 배열로 응답해"라고 했는데, 실제로는 이런 식으로 답한다:
```json
[
{ "resource_id": "...", "severity": "CRITICAL", ... },
...
]
```
아니면 응답 끝에 "위 findings는 다음과 같은 의미를 갖습니다..." 같은 설명을 붙이거나. 단순 json.loads는 다 실패한다.
결국 3단계 파서를 만들었다:
1. strip() 후 [로 시작하면 직접 파싱
2. 마크다운 code fence를 greedy regex로 추출
3. 첫 [부터 bracket depth를 추적해 대응되는 ]를 찾아 파싱
이것도 완벽하진 않다. 응답이 max_tokens에서 잘리면 JSON이 닫히지 않아서 모든 전략이 실패한다. max_tokens를 4096 → 8192로 늘려볼까 했지만, 그러면 배치당 응답 시간이 더 길어진다. 트레이드오프.
리전마다 사용 가능한 모델이 다르고, cross-region inference profile이라는 게 있다.
anthropic.claude-sonnet-4-6 → ValidationExceptionapac.anthropic.claude-3-5-sonnet-20241022-v2:0 → OKus.anthropic.claude-sonnet-4-6 → OKapac., us., eu. 같은 prefix가 cross-region inference profile ID다. AWS 리전별로 어떤 profile을 지원하는지는 문서화가 산만해서 결국 스크립트로 전수 테스트했다. 최신 모델 쓰려면 기본적으로 us-east-1/us-west-2에 요청을 보내는 게 안전했다.
수집기 19개, 보안 표준 6개, FastAPI + Typer CLI, CSV/JSON/HTML/S3 출력. 기능적으로는 완성됐다.
실제 내 AWS 계정을 돌려봤을 때:
작은 계정이면 전체 스캔이 10분 내외로 끝난다. 하지만 큰 조직에선 토큰 쿼터에 금방 막힌다.
같은 입력에도 매번 다른 답이 나온다. 같은 IAM role을 두 번 분석시키면 한 번은 HIGH로, 한 번은 MEDIUM으로 나온다. 심지어 title도 달라진다. 이게 컴플라이언스 리포팅 용도로는 치명적이다. "지난주 리포트에 있던 이슈가 왜 없어졌지?"에 답할 수 없다.
temperature=0을 줘도 완전히 일관되지는 않는다. 실무에서 쓰려면 룰 기반 스캐너로 바닥을 먼저 다지고, LLM은 체이닝이나 컨텍스트 해석 레이어로만 얹는 게 맞다. 그게 원래 디자인 의도였는데 실제로 돌려보니 더 선명해졌다.
개별 자원 findings 수백 개를 던져놓고 "여기서 공격 경로를 찾아봐"라고 하면, 실제로 "public S3 bucket → Lambda code 탈취 → Lambda IAM role로 권한 상승" 같은 체인을 찾아낸다. 이건 룰 기반으로는 거의 불가능하다. 이 부분이 이 프로젝트의 진짜 가치가 될 수 있을 것 같다.
Claude Sonnet 4.6 기준으로 계정 하나 돌리는 데 대략 500~1000만 토큰이 든다. input $3/1M, output $15/1M 으로 잡으면 스캔 한 번에 $5~15. 매일 돌리면 월 $150~450. 중견 기업 AWS 환경 하나 돌릴 때. 룰 기반 오픈소스 스캐너는 무료.
요약/증분 분석 전략이 없으면 경제성이 안 맞는다.
작성했는데, 토큰 사용량이 너무 많고 검색이 느려서 아직 본격적인 프로덕션 환경에는 무리지만, 만드는 동안 고민 자체로 유의미한 시도였다. 다음에 시간될때 좀 더 다듬어볼 예정이다.
LLM 기반 보안 도구가 상용화되려면 풀어야 할 문제들이 많다는 걸 체감한 게 가장 큰 수확이다. 아이디어 레벨에서는 "LLM에 JSON 던지면 되지"라고 생각했지만, 실제로는 토큰 비용, 결정성, 파싱, 쿼터, 모델 리전 이슈까지 하나하나 다 엔지니어링이었다.
소스: https://github.com/lufianlee/aws-vul-scanner
스택: Python 3.11+, boto3, Pydantic, FastAPI, Typer, Amazon Bedrock (Claude Sonnet 4.6)