TL;DR
- Flan Scan là lightweight network vulnerability scanner Cloudflare open-source năm 2019 — chính nó wrap
nmapvới NSE scriptvulners.nserồi chạy trong Docker. Cloudflare dùng Flan để PCI scan internal range, không mua scanner thương mại.- Stack đơn giản:
nmap -sV -oXra service version → NSEvulnersscript lookup Vulners API → Python script parse XML → render report HTML/JSON/markdown. Không có daemon, không có web UI — chạy là xong.- Vì sao không mua Tenable/Qualys/Wiz: Cloudflare cần scriptable, self-host, không phụ thuộc vendor, output free-form để feed vào ticket system của họ. Tenable Nessus Pro ~$3.5k/năm/scanner, Qualys VMDR theo asset count đắt hơn nhiều khi range lớn.
- Limitation rõ ràng: Flan là network-level scanner (service banner, port). Không có authenticated scan, không có web app scan, không có container image scan. Pair với Trivy/Snyk cho container và Wiz/ZAP cho app layer.
- CI workflow tôi dùng: Flan chạy tuần một lần trên staging subnet trong GitHub Actions runner, diff CVE list với baseline, mở Linear issue nếu có CRITICAL mới. Không block merge — tránh false-positive làm dev nghỉ việc.
- Vulners API rate limit: free tier 100 req/day, đủ cho scan nhỏ. Production cần Vulners API key (~$50/tháng) hoặc tự host CVE feed.
- Output
flan_scan_results.htmlđủ đẹp để gửi cho compliance auditor — không cần làm lại slide.
Vì sao một scanner viết bằng shell + Python lại nằm trên 4k star
Năm 2019, Cloudflare publish bài blog “Introducing Flan Scan” cùng repo. Câu mở đầu của họ thẳng thừng: “Cloudflare needed to scan its network for vulnerabilities. We weren’t satisfied with the commercial options”. Đọc xong, tôi nghĩ “thêm một post marketing tool” — đến khi mở source ra. Toàn bộ Flan là khoảng 200 dòng Python + một Dockerfile + vài shell script. Không có magic, không có agent, không có cloud SaaS phía sau. Nó chỉ là wrapper sạch quanh hai thứ đã production-grade 20 năm: nmap và vulners.nse.
Đó chính là lý do nó hữu ích. Vendor enterprise scanner đóng vai trò “platform” — bạn trả tiền cho UI, cho database CVE riêng, cho ticketing tích hợp. Nhưng phần engine quét CVE hầu hết là gọi cùng các nguồn public: NVD, Vulners, MITRE. Flan thừa nhận thực tế đó và đưa engine ra dạng container 30MB chạy được trong CI.
Tôi vận hành Flan trên production network của một fintech Việt Nam suốt 2024 — scan range 10.0.0.0/16 mỗi tuần, ~150 host. Bài này là note sau một năm.
Bên trong Flan: nmap → vulners NSE → Python parser
Khi docker run image Flan, quy trình chạy đại loại:
1. nmap -sV -oX /tmp/scan.xml <targets>
2. NSE script `vulners.nse` đi kèm nmap query Vulners API
cho mỗi service+version phát hiện
3. parse.py đọc /tmp/scan.xml, group by host, render report
4. Output: /tmp/flan_scan_results.html (+ JSON + markdown)
vulners.nse là script cộng đồng có sẵn trong nmap từ phiên bản 7.x — Flan không tự viết script đó. Khi nmap xác định “host A chạy nginx 1.18.0”, script này gọi Vulners search:
GET https://vulners.com/api/v3/burp/software/?software=nginx&version=1.18.0
Trả về list CVE kèm CVSS score. NSE merge vào XML output của nmap. Python parser của Flan đọc XML và sinh report:
# Chạy đơn giản nhất
docker pull cloudflare/flan
echo "192.168.1.0/24" > shared/ips.txt
docker run -v $(pwd)/shared:/shared cloudflare/flan
# → shared/reports/flan_scan_results.html
Trên báo cáo HTML mỗi host xuất hiện một block:
host: 10.0.3.41
port 22/tcp - OpenSSH 7.4
CVE-2018-15473 (CVSS 5.3) - username enumeration
CVE-2020-15778 (CVSS 6.8) - scp command injection
port 443/tcp - nginx 1.14.0
CVE-2019-9511 (CVSS 7.5) - HTTP/2 DoS
...
Trông không sexy. Nhưng đây chính là output mà PCI auditor cần thấy: host, service, version, CVE, CVSS. Và nó là HTML render từ XML — không phải PDF khóa bản quyền của Nessus.
So sánh thẳng: Flan vs Tenable Nessus vs Qualys VMDR vs Wiz
Cuối 2023 tôi review 4 lựa chọn cho team. Bảng tổng kết:
| Tool | License/Năm (USD) | Cài đặt | Authenticated scan | Container scan | Cloud-native scan | Audit-ready report |
|---|---|---|---|---|---|---|
| Flan | $0 (Vulners $0-50/tháng) | Docker pull 30 giây | Không | Không | Không | HTML đủ cho PCI |
| Tenable Nessus Pro | ~$3,500/scanner | Agent + console | Có | Có (tích hợp Tenable.io thêm tiền) | Một phần | Có template PCI/HIPAA |
| Qualys VMDR | ~$10k+ theo asset | Cloud agent + scanner appliance | Có | Có (TotalCloud) | Có | Compliance pack riêng |
| Wiz | Theo workload, $20k+/năm | Agentless, role-based vào AWS/GCP/Azure | N/A | Có | Có (mạnh nhất nhóm) | Có |
Quyết định: Flan cho network-level scan internal. Wiz cho cloud workload scan vì agentless approach phù hợp môi trường EKS đa-account. Không mua Tenable/Qualys vì overlap quá nhiều với hai cái trên và chi phí không justify cho scope hiện tại.
Nói rõ: Flan không thay Wiz. Nó thay phần “network vulnerability scanner” trong PCI Requirement 11.3 — tức quét external và internal IP, tìm CVE trong service exposed. Wiz làm việc khác (cloud config + workload). Hai tool bổ sung, không cạnh tranh.
Vulners API key — quyết định mua hay không
Flan dựa hoàn toàn vào Vulners API cho phần lookup CVE. Vulners là database vulnerability commercial nhưng có tier free. Theo doc Vulners thời điểm 2024:
- Anonymous: 100 request/day
- Free account (đăng ký): 1,000 request/day
- Personal API ($50/tháng): 25,000 request/day + commercial CVE feed
Mỗi service+version Flan phát hiện = 1 API call. Scan /16 với 150 host live trung bình 800-1,200 service version → cần Personal key. Tôi đăng ký tháng $50, set env VULNERS_API_KEY trong Docker run:
docker run \
-v $(pwd)/shared:/shared \
-e VULNERS_API_KEY=$VULNERS_API_KEY \
cloudflare/flan
Nếu range nhỏ (~30 host) thì free tier đủ. Nếu range lớn (~500+ host), tự host vuls.io hoặc dùng NVD feed offline đáng cân nhắc — không cần Vulners.
CI/CD integration: scan staging tuần một lần
Cách tôi dùng Flan trong production hiện tại. Một GitHub Actions workflow .github/workflows/flan-weekly.yml:
name: Weekly Flan scan
on:
schedule:
- cron: '0 2 * * 1' # 9 sáng thứ 2 GMT+7
workflow_dispatch:
jobs:
scan:
runs-on: [self-hosted, vpn-staging] # runner trong VPN staging
steps:
- uses: actions/checkout@v4
- name: Run Flan
run: |
mkdir -p shared
cp targets/staging-cidr.txt shared/ips.txt
docker run --rm \
-v ${PWD}/shared:/shared \
-e VULNERS_API_KEY=${{ secrets.VULNERS_API_KEY }} \
cloudflare/flan
- name: Diff vs baseline
run: |
python3 ci/diff-cve.py \
--baseline ci/baseline-cve.json \
--current shared/reports/flan_scan_results.json \
--severity CRITICAL,HIGH \
--output /tmp/new-cves.json
- name: Open Linear issue if new CRITICAL
if: success()
run: |
[ -s /tmp/new-cves.json ] && \
python3 ci/linear-create.py /tmp/new-cves.json || \
echo "No new findings"
- name: Upload report
uses: actions/upload-artifact@v4
with:
name: flan-report-${{ github.run_id }}
path: shared/reports/
retention-days: 90
Key design choice: không fail workflow, chỉ tạo ticket Linear. Lý do — Flan có false-positive (Vulners API có thể trả CVE đã được vendor backport patch nhưng version banner chưa đổi). Nếu fail merge mỗi lần có CVE mới, dev sẽ disable workflow trong tuần đầu. Mở ticket → security team triage → quyết định fix hay accept risk → đóng/khóa CVE đó trong baseline.
ci/diff-cve.py đơn giản:
# So sánh CVE phát hiện hiện tại với baseline đã accept
import json, sys
baseline = json.load(open(sys.argv[1].split('=')[1]))
current = json.load(open(sys.argv[2].split('=')[1]))
baseline_ids = {f"{h['ip']}/{c['id']}" for h in baseline for c in h.get('vulnerabilities', [])}
new = []
for host in current:
for cve in host.get('vulnerabilities', []):
key = f"{host['ip']}/{cve['id']}"
if key not in baseline_ids and cve['severity'] in ('CRITICAL', 'HIGH'):
new.append({'host': host['ip'], 'cve': cve})
json.dump(new, open('/tmp/new-cves.json', 'w'))
Baseline được update mỗi quý sau khi security team review.
Limitation cần biết trước khi tin Flan
1. Banner-based detection sai khi vendor backport patch. Ubuntu/RHEL hay patch CVE nhưng giữ version string cũ trong banner. Ví dụ OpenSSH 7.4 trên RHEL 7 có thể đã fix CVE-2018-15473 từ lâu nhưng banner vẫn show “7.4”. Flan sẽ flag false-positive. Workaround: filter theo OS (os_match của nmap) hoặc dùng authenticated scan (mà Flan không có).
2. Không scan được service không phản hồi banner. Database internal yêu cầu auth ngay từ packet đầu → nmap không có version → không CVE lookup. Phần lớn mysql/postgres modern nằm dạng này. Pair với CIS Benchmarks scan riêng.
3. Không quét app layer. Web app vulnerability (XSS, SQLi, IDOR) Flan không thấy. Cần OWASP ZAP hoặc Nuclei bổ sung.
4. Container image không scan được. Flan scan running service trên network. Image ở registry phải dùng Trivy hoặc Grype. Đây là điểm modern container scanner mạnh hơn — họ scan layer trước khi deploy.
5. Repo không active development. Flan repo Cloudflare chủ yếu maintenance mode — commit cuối cùng vài tháng một lần, chủ yếu bump dependency. Đừng kỳ vọng feature mới. Nếu cần custom (output Slack, integrate Splunk), tự fork.
Một workflow đầy đủ: Flan trong layered scanning
Flan không phải end-all. Trong setup của tôi nó là một mảnh trong layered approach:
| Layer | Tool | Tần suất | Output |
|---|---|---|---|
| Container image (pre-deploy) | Trivy trong CI | Mỗi PR | Block merge nếu CRITICAL |
| Cloud config (AWS/GCP) | Wiz/Prowler | Liên tục | Slack alert |
| Network service (internal/external) | Flan | Tuần 1 lần | Linear ticket |
| Web app | OWASP ZAP baseline | Mỗi staging deploy | Junit report vào PR |
| Secret in code | Gitleaks pre-commit + CI | Mỗi commit | Block push |
| OS/CIS benchmark | OpenSCAP/Lynis | Tháng 1 lần | PDF cho audit |
Flan ngồi ở row 3 — không trùng với row khác, không bị row khác cover. Mỗi tool chỉ chuyên một lớp.
Khi Cloudflare nói “we scan our own network with Flan”
Đoạn này thường bị hiểu sai. Cloudflare có team security riêng và họ có nhiều tool nội bộ. Flan không phải tool duy nhất họ dùng — nó là một mảnh trong vulnerability management program. Theo bài Cloudflare blog post 2019, Flan giúp họ thay phần “PCI external scan” mà trước đó thuê vendor (mỗi quý PCI DSS yêu cầu external scan, vendor charge per IP). Open-source nó vì họ không bán scanner làm sản phẩm — nên không có conflict.
Tôi dùng Flan với tâm thế tương tự. Nó không thay vendor SaaS. Nó thay phần internal scanning thủ công mà trước đó tôi chạy nmap -sV thủ công + tự lookup CVE — quá tốn thời gian, không reproducible.
Bottom line
Flan là một wrapper Python + Docker quanh nmap và vulners.nse. Đó là điểm mạnh — không có magic, code đọc 1 tiếng hiểu hết, không có vendor lock-in. Nếu bạn cần network vulnerability scanner self-host cho PCI scope hoặc internal audit, Flan đủ. Nếu bạn cần authenticated scan, container image scan, hay cloud config scan, dùng tool khác — và đừng cố ép Flan làm. Đối với tôi, sau một năm vận hành, ROI rõ ràng: không tốn license, không tốn người vận hành (workflow tự chạy), output đủ tốt để gửi compliance auditor. Thay được Tenable Nessus Pro $3.5k/năm cho network-level scan.
Checklist trước production
- Vulners API key (Personal $50/tháng) đã có nếu scan > 50 host
-
targets/staging-cidr.txtchuẩn — không include production network nếu chưa change-approve - CI runner trong cùng VLAN với target hoặc qua VPN — Flan không pivot
- Baseline CVE JSON đã review và check-in repo
- Linear/Jira webhook integration test ít nhất một lần
- Retention artifact >= 90 ngày cho audit trail
- Schedule scan ngoài giờ peak (ví dụ thứ 2 sáng GMT+7) — nmap dễ làm tăng latency
- Khi onboard service mới vào range, manually scan trước khi vào weekly schedule
- Document quy trình “accept risk” — thêm vào baseline phải có ticket approve
- Pair với Trivy (image) + Wiz/Prowler (cloud config) + ZAP (web app)
- Test alert path quý 1 lần — fake một CVE, đảm bảo ticket được tạo
Cạm bẫy thường gặp
1. Quét production từ internet vào (cho PCI external scan) cần CIDR egress IP cố định. Mở firewall theo IP của GitHub-hosted runner = không khả thi, IP thay đổi. Dùng self-hosted runner với EIP cố định.
2. False-positive khi RHEL/Ubuntu backport patch. Đừng fail CI vì điều này. Chỉ tạo ticket cho human review.
3. Vulners API key leak. Đừng paste vào docker run literal — luôn -e VULNERS_API_KEY=$VAR để key không xuất hiện trong docker inspect.
4. Quét UDP rất chậm. Mặc định Flan chỉ scan TCP. Nếu cần UDP, sửa NSE arg, nhưng scan UDP /24 có thể 6-12 giờ. Cân nhắc giới hạn port (-sU -p 53,123,161,500).
5. NSE script vulners không tự update. Phụ thuộc nmap version trong image. Re-pull image quý 1 lần để có script mới nhất.