#!/usr/bin/env python3 """경로에 실행 파일이 아니라 스크립트가 박힌 자세 검사를 찾아냅니다 (운영 매뉴얼 M8). 🟢 읽기만 합니다. 아무것도 바꾸지 않습니다. export CF_API_TOKEN=<읽기 전용 토큰>; export CF_ACCOUNT_ID=<계정 ID> python posture.py 무엇을 찾나 — Application check 의 경로에 셸 스크립트·배치 파일을 넣으면 **실행 중이어도 「아니오」로 나옵니다.** 검사가 보는 것은 실행 중 프로세스의 실제 실행 파일 경로(리눅스면 /proc//exe)여서, 스크립트를 돌리면 그 값이 해석기(/bin/sh)가 되어 지정한 경로와 안 맞기 때문입니다. 🔵 단말이 실제로 어떻게 판정됐는지(running · sha256)를 주는 **공개 API 는 없습니다** (2026-09-11 OpenAPI 전수 확인). 그것은 대시보드에서 봅니다. 이 코드는 **설정 쪽**을 봅니다 — 단말을 한 대씩 열어 볼 수 없는 것을 한 번에 봅니다. 공식 문서 자세 검사 API https://developers.cloudflare.com/api/resources/zero_trust/subresources/devices/subresources/posture/methods/list/ Application check https://developers.cloudflare.com/cloudflare-one/reusable-components/posture-checks/client-checks/application-check/ """ import json, os, sys, 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 를 환경변수로 넣어 주세요.") SCRIPT_EXT = (".sh", ".bash", ".zsh", ".ksh", ".py", ".rb", ".pl", ".php", ".lua", ".bat", ".cmd", ".ps1", ".vbs", ".js", ".jar") WIN_EXEC_EXT = (".exe", ".com", ".msi", ".dll", ".sys") def get(path): req = urllib.request.Request(API + "/accounts/" + ACCOUNT + path) req.add_header("Authorization", "Bearer " + TOKEN) with urllib.request.urlopen(req, timeout=30) as r: return json.loads(r.read().decode("utf-8")).get("result") or [] def posture_uids(o, out): """정책 안 어디에 있든 device_posture.integration_uid 를 긁어 옵니다. (Access 정책의 자세 검사 조건은 {"device_posture": {"integration_uid": "<검사 id>"}} 모양입니다.)""" if isinstance(o, dict): dp = o.get("device_posture") if isinstance(dp, dict) and dp.get("integration_uid"): out.add(str(dp["integration_uid"])) for v in o.values(): posture_uids(v, out) elif isinstance(o, list): for v in o: posture_uids(v, out) rules = get("/devices/posture") path_rules = [r for r in rules if r.get("type") in ("application", "file")] print("자세 검사 %d개 중 경로를 보는 것 %d개 (Application check · File check)" % (len(rules), len(path_rules))) if not path_rules: print("경로형 검사가 없습니다 — 아직 이 함정에 빠질 자리가 없습니다.") sys.exit(0) # 어느 정책이 이 검사를 물고 있는지 — 판정이 뒤집히면 막히는 것이 그것입니다. used = {} for path, label in (("/access/policies", "재사용 정책"), ("/access/apps", "앱")): try: for item in get(path): got = set() posture_uids(item, got) for u in got: used.setdefault(u, []).append("%s 「%s」" % (label, item.get("name") or item.get("id"))) except Exception as e: print(" ⚠️ %s 를 못 읽었습니다(%s) — 사용처는 비워 둡니다" % (path, str(e)[:50])) bad = 0 for r in path_rules: inp = r.get("input") or {} p = str(inp.get("path") or "") osname = str(inp.get("operating_system") or "-") low = p.lower() kind = "Application check(돌고 있나)" if r.get("type") == "application" else "File check(설치돼 있나)" if low.endswith(SCRIPT_EXT): mark = "🔴" why = ("스크립트 경로입니다. 실행 중이어도 running:false 로 나옵니다 — 검사가 보는 것은 " "실행 중 프로세스의 실제 실행 파일이라 여기서는 해석기(/bin/sh 등)가 잡힙니다.") elif osname == "windows" and low and not low.endswith(WIN_EXEC_EXT): mark, why = "⚠️", "윈도우인데 실행 파일 확장자가 아닙니다. 바로가기·스크립트면 안 잡힙니다." else: mark, why = "🟢", "" if mark != "🟢": bad += 1 print("\n%s %s" % (mark, r.get("name") or r.get("id"))) print(" %s · OS %s" % (kind, osname)) print(" 경로 %s" % (p or "(없음)")) where = used.get(str(r.get("id")), []) print(" 쓰는 곳: " + (", ".join(where[:4]) if where else "없음 — 만들어만 두고 아무 정책에도 안 걸었습니다")) if why: print(" → " + why) print("") if bad: print("🔴 손봐야 할 검사 %d개. 경로를 진짜 실행 파일로 바꾸십시오." % bad) print(" 「설치돼 있나」만 보고 싶은 것이라면 Application check 가 아니라 File check 를 씁니다 —") print(" 파일이 있으면 돌든 안 돌든 sha256 이 계산됩니다. 다른 신호입니다.") else: print("🟢 스크립트 경로로 잘못 건 검사는 없습니다.")