Bài này dựng một agent SAST chạy được từ đầu: cấu trúc thư mục, biến môi trường, và vòng lặp gọi LLM có tool. Ta dùng Python cho ví dụ (dễ đọc), nhưng nguyên lý áp dụng cho mọi ngôn ngữ.
1. Cấu trúc dự án
sast-agent/
agent/
scan.py # điều phối quét
llm.py # wrapper gọi LLM
tools.py # đọc file, tra schema
verify.py # agent phản biện
rank.py # chấm severity
prompts/
finder.md # prompt tìm lỗ hổng
verifier.md # prompt phản biện
.env
requirements.txt
Tách prompt ra file .md để version-control và sửa mà không đụng code.
2. Biến môi trường
# .env
LLM_API_KEY=sk-xxxx
LLM_MODEL=your-reasoning-model
MAX_TOKENS=4096
TEMPERATURE=0.1 # thấp để ổn định, ít bịa
SCAN_MODE=diff # diff | full
MIN_CONFIDENCE=0.6 # ngưỡng lọc finding
TEMPERATURE để thấp (0–0.2) vì ta cần tính nhất quán, không cần sáng tạo. Đây là quy tắc quan trọng cho mọi tác vụ phân tích bảo mật.
3. Wrapper LLM tối thiểu
# agent/llm.py
import os, json
from openai import OpenAI # hoặc SDK tương đương
client = OpenAI(api_key=os.environ["LLM_API_KEY"])
def ask(system: str, user: str) -> dict:
resp = client.chat.completions.create(
model=os.environ["LLM_MODEL"],
temperature=float(os.environ["TEMPERATURE"]),
response_format={"type": "json_object"}, # ép JSON
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user},
],
)
return json.loads(resp.choices[0].message.content)
Ép response_format JSON để output máy đọc được, không phải văn xuôi. Đây là điều kiện tiên quyết để pipeline tự động.
4. Prompt tìm lỗ hổng (finder)
<!-- prompts/finder.md -->
Bạn là chuyên gia AppSec. Phân tích đoạn code sau và chỉ báo
lỗ hổng SQLi, XSS, hoặc IDOR mà bạn CHẮC CHẮN có đường đi
Source -> Sink KHÔNG qua Sanitizer đúng loại.
Trả về JSON:
{"findings": [{
"type": "SQLi|XSS|IDOR",
"line": <số dòng>,
"source": "<biến/đầu vào bẩn>",
"sink": "<nơi nguy hiểm>",
"why": "<giải thích ngắn>",
"confidence": <0..1>
}]}
Nếu không chắc, để confidence thấp. KHÔNG bịa dòng code không có.
5. Vòng điều phối quét
# agent/scan.py
from agent.llm import ask
from pathlib import Path
FINDER = Path("prompts/finder.md").read_text()
def scan_file(path: str) -> list:
code = Path(path).read_text()
numbered = "\n".join(f"{i+1}: {l}"
for i, l in enumerate(code.splitlines()))
out = ask(FINDER, f"File: {path}\n\n{numbered}\n``")
return out.get("findings", [])
`Đánh số dòng trước khi đưa vào prompt giúp LLM chỉ đúng vị trí — mẹo nhỏ nhưng cực kỳ hữu ích khi xuất báo cáo.
Một lưu ý về chunking: file điển hình vài trăm dòng thì đưa cả file vào được, nhưng file nghìn dòng sẽ vượt ngân sách và làm model "loãng" sự chú ý. Chiến lược tốt là cắt theo ranh giới hàm hoặc route — mỗi đơn vị phân tích là một hàm cùng các hàm nó gọi trực tiếp. Cách này vừa giữ được ngữ cảnh cần thiết cho taint tracking, vừa không đốt token vô ích vào phần code không liên quan. Ở các bài sau, bạn sẽ thấy vòng lặp "cấp thêm ngữ cảnh" cho phép agent tự yêu cầu định nghĩa hàm còn thiếu thay vì phải nhồi sẵn cả file.
6. Các bước chạy thử
pip install -r requirements.txt (SDK LLM + python-dotenv).
Tạo .env với key thật, TEMPERATURE=0.1.
Chuẩn bị một file mẫu có lỗi SQLi cố ý.
python -c "from agent.scan import scan_file; print(scan_file('sample.php'))".
Kiểm tra output JSON có đúng dòng và loại lỗ hổng không.
Chạy lại 3 lần để xem tính nhất quán (nên giống nhau vì temperature thấp).
Checklist môi trường
- [ ] Prompt nằm ở file riêng, có version.
- [ ] Output ép JSON, có schema rõ ràng.
- [ ] Temperature ≤ 0.2.
- [ ] Có đánh số dòng.
- [ ] Có ngưỡng
MIN_CONFIDENCE` để lọc.
Sai lầm thường gặp
- Để temperature mặc định (0.7–1.0): kết quả nhảy loạn giữa các lần chạy, không thể dùng trong CI.
- Nhận output văn xuôi rồi regex: mong manh; luôn ép JSON.
- Nhét key API vào code: rò rỉ secret. Dùng biến môi trường / secret manager.
- Quên đánh số dòng: LLM chỉ "khoảng dòng 40 gì đó", báo cáo vô dụng.