#!/usr/bin/env python3 """「클라이언트를 설치하지 않으면 못 들어오게」가 정말 그렇게 되어 있나 (운영 매뉴얼 M17). 🟢 읽기만 합니다. 아무것도 바꾸지 않습니다. 표준 라이브러리만 씁니다 — 설치할 것이 없습니다. export CF_API_TOKEN=<읽기 전용 토큰> # 권한: Access Read export CF_ACCOUNT_ID=<계정 ID> python require.py 무엇을 보나 — 앱마다 정책의 **require** 에 무엇이 걸려 있는지 모아, 두 가지를 가릅니다. warp 「Cloudflare One 클라이언트로 붙었나」 🔴 개인용 WARP 도 통과합니다 gateway 「우리 조직의 Gateway 를 지나왔나」 조직 프로필이 걸린 단말만 통과합니다 🔴 **이것이 제일 비싼 오해입니다.** 「회사 PC 만 들어오게」가 목적이라면 require 는 **gateway** 여야 합니다. warp 만 걸어 두면 집에서 개인용 WARP 를 켠 사설 노트북도 통과합니다 — 공식 문서가 그렇게 적고 있습니다(including the consumer version). 같이 봐야 하는 것 — 기기 자세 검사(device_posture)를 require 에 걸어 두면 「회사가 관리하는 단말」에 훨씬 가까워집니다. 이 스크립트는 그 항목도 함께 세어 보여 줍니다. 공식 문서 Require 규칙 https://developers.cloudflare.com/cloudflare-one/access-controls/policies/ WARP 확인 https://developers.cloudflare.com/cloudflare-one/access-controls/policies/#require-cloudflare-one-client Gateway 확인 https://developers.cloudflare.com/cloudflare-one/access-controls/policies/#require-gateway 정책 목록 https://developers.cloudflare.com/api/resources/zero_trust/subresources/access/subresources/policies/methods/list/ """ import json import os import sys import urllib.error import urllib.parse import urllib.request API = "https://api.cloudflare.com/client/v4" TOKEN = os.environ.get("CF_API_TOKEN", "") ACCOUNT = os.environ.get("CF_ACCOUNT_ID", "") if not TOKEN or not ACCOUNT: sys.exit("CF_API_TOKEN 과 CF_ACCOUNT_ID 를 환경변수로 넣어 주세요.") def call(path, params=None, quiet=False): url = API + "/accounts/" + ACCOUNT + path if params: url += "?" + urllib.parse.urlencode(params) req = urllib.request.Request(url) req.add_header("Authorization", "Bearer " + TOKEN) try: with urllib.request.urlopen(req, timeout=30) as r: return json.loads(r.read().decode("utf-8")) except urllib.error.HTTPError as e: if quiet: return None raw = e.read().decode("utf-8", "replace") try: msg = json.dumps(json.loads(raw).get("errors"), ensure_ascii=False) except ValueError: msg = raw[:200] sys.exit("GET %s → HTTP %d %s" % (path, e.code, msg)) def get_all(path): out, cursor = [], "" for _ in range(50): q = {"per_page": "100"} if cursor: q["cursor"] = cursor j = call(path, q) batch = j.get("result") or [] out.extend(batch) nxt = (j.get("result_info") or {}).get("cursor") or "" if not nxt or nxt == cursor or not batch: break cursor = nxt return out def kinds(rules): """require/include 규칙 목록에서 종류만 뽑습니다(중첩된 any/all 도 펼칩니다).""" found = [] stack = list(rules or []) while stack: r = stack.pop() if not isinstance(r, dict): continue for k, v in r.items(): if k in ("any_valid_service_token", "certificate"): found.append(k) elif isinstance(v, dict): found.append(k) stack.extend([v]) elif isinstance(v, list): stack.extend([x for x in v if isinstance(x, dict)]) else: found.append(k) return found apps = get_all("/access/apps") rows, warp_only, gateway_apps, no_require = [], [], [], [] for a in apps: pols = a.get("policies") or [] if not pols: pols = call("/access/apps/%s/policies" % a.get("id"), quiet=True) pols = (pols or {}).get("result") or [] req = [] for p in pols: req.extend(kinds(p.get("require"))) name = str(a.get("name") or a.get("id"))[:32] has_warp = "warp" in req has_gw = "gateway" in req has_posture = "device_posture" in req rows.append((name, a.get("type") or "-", has_warp, has_gw, has_posture, len(pols))) if has_warp and not has_gw: warp_only.append(name) if has_gw: gateway_apps.append(name) if not (has_warp or has_gw or has_posture): no_require.append(name) print("앱 %d개 — require 에 무엇이 걸려 있나\n" % len(rows)) print("%-32s %-14s %-8s %-9s %-9s %s" % ("앱", "종류", "warp", "gateway", "자세검사", "정책 수")) print("-" * 92) for name, typ, w, g, po, n in rows: print("%-32s %-14s %-8s %-9s %-9s %d" % ( name, str(typ)[:14], "예" if w else "-", "예" if g else "-", "예" if po else "-", n)) print("\n판정") if warp_only: print("🔴 warp 만 걸린 앱 %d개 — **개인용 WARP 로도 통과합니다**: %s" % (len(warp_only), ", ".join(warp_only[:8]))) print(" 「회사 PC 만」이 목적이라면 require 를 gateway 로 바꾸거나 함께 거십시오.") else: print("🟢 warp 만 걸린 앱은 없습니다.") if gateway_apps: print("🟢 gateway 가 걸린 앱 %d개: %s" % (len(gateway_apps), ", ".join(gateway_apps[:8]))) if no_require: print("⚠️ 단말 조건이 아예 없는 앱 %d개 — 신원만 맞으면 어떤 기기로도 들어옵니다: %s" % (len(no_require), ", ".join(no_require[:8]))) print("\n자세 검사(device_posture)를 함께 걸면 「회사가 관리하는 단말」에 더 가까워집니다.")