Jamf Pro 컨텍스트 로그 수집
이 문서에서는 Lambda 및 EventBridge 일정을 사용하여 AWS S3를 통해 Jamf Pro 컨텍스트 로그 (기기 및 사용자 컨텍스트)를 (Google Security Operations)에 수집하는 방법을 설명합니다.
시작하기 전에
- Google SecOps 인스턴스
- Jamf Pro 테넌트에 대한 액세스 권한
- AWS (S3, IAM, Lambda, EventBridge)에 대한 권한 있는 액세스
Jamf API 역할 구성
- Jamf 웹 UI에 로그인합니다.
- 설정 > 시스템 섹션 > API 역할 및 클라이언트로 이동합니다.
- API 역할 탭을 선택합니다.
- New(새로 만들기)를 클릭합니다.
- API 역할의 표시 이름 (예:
context_role
)을 입력합니다. Jamf Pro API 역할 권한에 권한 이름을 입력한 다음 메뉴에서 선택합니다.
- 컴퓨터 인벤토리
- 휴대기기 인벤토리
저장을 클릭합니다.
Jamf API 클라이언트 구성
- Jamf Pro에서 설정 > 시스템 섹션 > API 역할 및 클라이언트로 이동합니다.
- APl 클라이언트 탭을 선택합니다.
- New(새로 만들기)를 클릭합니다.
- API 클라이언트의 표시 이름 (예:
context_client
)을 입력합니다. - API 역할 필드에 이전에 만든
context_role
역할을 추가합니다. - 액세스 토큰 수명에 액세스 토큰이 유효한 시간을 초 단위로 입력합니다.
- 저장을 클릭합니다.
- 수정을 클릭합니다.
- API 클라이언트 사용 설정을 클릭합니다.
- 저장을 클릭합니다.
Jamf 클라이언트 보안 비밀번호 구성
- Jamf Pro에서 새로 만든 API 클라이언트로 이동합니다.
- 클라이언트 보안 비밀 생성을 클릭합니다.
- 확인 화면에서 보안 비밀 만들기를 클릭합니다.
- 안전한 위치에 다음 매개변수를 저장합니다.
- 기본 URL:
https://<your>.jamfcloud.com
- 클라이언트 ID: UUID입니다.
- 클라이언트 보안 비밀번호: 값은 한 번만 표시됩니다.
- 기본 URL:
Google SecOps용 AWS S3 버킷 및 IAM 구성
- 이 사용자 가이드(버킷 만들기)에 따라 Amazon S3 버킷을 만듭니다.
- 나중에 참조할 수 있도록 버킷 이름과 리전을 저장합니다 (예:
jamfpro
). - 이 사용자 가이드(IAM 사용자 만들기)에 따라 사용자를 만듭니다.
- 생성된 사용자를 선택합니다.
- 보안 사용자 인증 정보 탭을 선택합니다.
- 액세스 키 섹션에서 액세스 키 만들기를 클릭합니다.
- 사용 사례로 서드 파티 서비스를 선택합니다.
- 다음을 클릭합니다.
- 선택사항: 설명 태그를 추가합니다.
- 액세스 키 만들기를 클릭합니다.
- 액세스 키와 비밀 액세스 키를 저장하여 나중에 참고하려면 CSV 파일 다운로드를 클릭합니다.
- 완료를 클릭합니다.
- 권한 탭을 선택합니다.
- 권한 정책 섹션에서 권한 추가를 클릭합니다.
- 권한 추가를 선택합니다.
- 정책 직접 연결을 선택합니다.
- AmazonS3FullAccess 정책을 검색하여 선택합니다.
- 다음을 클릭합니다.
- 권한 추가를 클릭합니다.
S3 업로드용 IAM 정책 및 역할 구성
정책 JSON (다른 버킷 이름을 입력한 경우
jamfpro
대체):{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPutJamfObjects", "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::jamfpro/*" } ] }
AWS 콘솔 > IAM > 정책 > 정책 생성 > JSON 탭으로 이동합니다.
정책을 복사하여 붙여넣습니다.
다음 > 정책 만들기를 클릭합니다.
IAM > 역할 > 역할 생성 > AWS 서비스 > Lambda로 이동합니다.
새로 만든 정책을 연결합니다.
역할 이름을
WriteJamfToS3Role
로 지정하고 역할 만들기를 클릭합니다.
Lambda 함수 만들기
- AWS 콘솔에서 Lambda > 함수 > 함수 만들기로 이동합니다.
- 처음부터 작성을 클릭합니다.
- 다음 구성 세부정보를 제공합니다.
설정 | 값 |
---|---|
이름 | jamf_pro_to_s3 |
런타임 | Python 3.13 |
아키텍처 | x86_64 |
권한 | WriteJamfToS3Role |
함수가 생성되면 코드 탭을 열고 스텁을 삭제하고 다음 코드를 입력합니다 (
jamf_pro_to_s3.py
).import os import io import json import gzip import time import logging from datetime import datetime, timezone import boto3 import requests log = logging.getLogger() log.setLevel(logging.INFO) BASE_URL = os.environ.get("JAMF_BASE_URL", "").rstrip("/") CLIENT_ID = os.environ.get("JAMF_CLIENT_ID") CLIENT_SECRET = os.environ.get("JAMF_CLIENT_SECRET") S3_BUCKET = os.environ.get("S3_BUCKET") S3_PREFIX = os.environ.get("S3_PREFIX", "jamf-pro/context/") PAGE_SIZE = int(os.environ.get("PAGE_SIZE", "200")) SECTIONS = [ "GENERAL", "HARDWARE", "OPERATING_SYSTEM", "USER_AND_LOCATION", "DISK_ENCRYPTION", "SECURITY", "EXTENSION_ATTRIBUTES", "APPLICATIONS", "CONFIGURATION_PROFILES", "LOCAL_USER_ACCOUNTS", "CERTIFICATES", "SERVICES", "PRINTERS", "SOFTWARE_UPDATES", "GROUP_MEMBERSHIPS", "CONTENT_CACHING", "STORAGE", "FONTS", "PACKAGE_RECEIPTS", "PLUGINS", "ATTACHMENTS", "LICENSED_SOFTWARE", "IBEACONS", "PURCHASING", ] s3 = boto3.client("s3") def _now_iso(): return datetime.now(timezone.utc).isoformat() def get_token(): """OAuth2 client credentials > access_token""" url = f"{BASE_URL}/api/oauth/token" data = { "grant_type": "client_credentials", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, } headers = {"Content-Type": "application/x-www-form-urlencoded"} r = requests.post(url, data=data, headers=headers, timeout=30) r.raise_for_status() j = r.json() return j["access_token"], int(j.get("expires_in", 1200)) def fetch_page(token: str, page: int): """GET /api/v1/computers-inventory with sections & pagination""" url = f"{BASE_URL}/api/v1/computers-inventory" params = [("page", page), ("page-size", PAGE_SIZE)] + [("section", s) for s in SECTIONS] hdrs = {"Authorization": f"Bearer {token}", "Accept": "application/json"} r = requests.get(url, params=params, headers=hdrs, timeout=60) r.raise_for_status() return r.json() def to_context_event(item: dict) -> dict: inv = item.get("inventory", {}) or {} general = inv.get("general", {}) or {} hardware = inv.get("hardware", {}) or {} osinfo = inv.get("operatingSystem", {}) or {} loc = inv.get("location", {}) or inv.get("userAndLocation", {}) or {} computer = { "udid": general.get("udid") or hardware.get("udid"), "deviceName": general.get("name") or general.get("deviceName"), "serialNumber": hardware.get("serialNumber") or general.get("serialNumber"), "model": hardware.get("model") or general.get("model"), "osVersion": osinfo.get("version") or general.get("osVersion"), "osBuild": osinfo.get("build") or general.get("osBuild"), "macAddress": hardware.get("macAddress"), "alternateMacAddress": hardware.get("wifiMacAddress"), "ipAddress": general.get("ipAddress"), "reportedIpV4Address": general.get("reportedIpV4Address"), "reportedIpV6Address": general.get("reportedIpV6Address"), "modelIdentifier": hardware.get("modelIdentifier"), "assetTag": general.get("assetTag"), } user_block = { "userDirectoryID": loc.get("username") or loc.get("userDirectoryId"), "emailAddress": loc.get("emailAddress"), "realName": loc.get("realName"), "phone": loc.get("phone") or loc.get("phoneNumber"), "position": loc.get("position"), "department": loc.get("department"), "building": loc.get("building"), "room": loc.get("room"), } return { "webhook": {"name": "api.inventory"}, "event_type": "ComputerInventory", "event_action": "snapshot", "event_timestamp": _now_iso(), "event_data": { "computer": {k: v for k, v in computer.items() if v not in (None, "")}, **{k: v for k, v in user_block.items() if v not in (None, "")}, }, "_jamf": { "id": item.get("id"), "inventory": inv, }, } def write_ndjson_gz(objs, when: datetime): buf = io.BytesIO() with gzip.GzipFile(filename="-", mode="wb", fileobj=buf, mtime=int(time.time())) as gz: for obj in objs: line = json.dumps(obj, separators=(",", ":")) + "\n" gz.write(line.encode("utf-8")) buf.seek(0) prefix = S3_PREFIX.strip("/") + "/" if S3_PREFIX else "" key = f"{prefix}{when:%Y/%m/%d}/jamf_pro_context_{int(when.timestamp())}.ndjson.gz" s3.put_object(Bucket=S3_BUCKET, Key=key, Body=buf.getvalue()) return key def lambda_handler(event, context): assert BASE_URL and CLIENT_ID and CLIENT_SECRET and S3_BUCKET, "Missing required env vars" token, _ttl = get_token() page = 0 total = 0 batch = [] now = datetime.now(timezone.utc) while True: payload = fetch_page(token, page) results = payload.get("results") or payload.get("computerInventoryList") or [] if not results: break for item in results: batch.append(to_context_event(item)) total += 1 if len(batch) >= 5000: key = write_ndjson_gz(batch, now) log.info("wrote %s records to s3://%s/%s", len(batch), S3_BUCKET, key) batch = [] if len(results) < PAGE_SIZE: break page += 1 if batch: key = write_ndjson_gz(batch, now) log.info("wrote %s records to s3://%s/%s", len(batch), S3_BUCKET, key) return {"ok": True, "count": total}
구성 > 환경 변수 > 수정 > 새 환경 변수 추가로 이동합니다.
다음 환경 변수를 입력하고 사용자 값으로 바꿉니다.
환경 변수
키 예 S3_BUCKET
jamfpro
S3_PREFIX
jamf-pro/context/
AWS_REGION
지역 선택 JAMF_CLIENT_ID
Jamf 클라이언트 ID 입력 JAMF_CLIENT_SECRET
Jamf 클라이언트 보안 비밀번호 입력 JAMF_BASE_URL
Jamf URL을 입력하고 https://<your>.jamfcloud.com
에서<your>
를 바꿉니다.PAGE_SIZE
200
함수가 생성된 후 해당 페이지에 머무르거나 Lambda > 함수 > your-function을 엽니다.
구성 탭을 선택합니다.
일반 구성 패널에서 수정을 클릭합니다.
제한 시간을 5분 (300초)으로 변경하고 저장을 클릭합니다.
EventBridge 일정 만들기
- Amazon EventBridge > 스케줄러 > 일정 만들기로 이동합니다.
- 다음 구성 세부정보를 제공합니다.
- 반복 일정: 요금 (
1 hour
) - 타겟: Lambda 함수
- 이름:
jamfpro-context-schedule-1h
- 반복 일정: 요금 (
- 일정 만들기를 클릭합니다.
Jamf Pro 컨텍스트 로그를 수집하도록 Google SecOps에서 피드 구성
- SIEM 설정> 피드로 이동합니다.
- + 새 피드 추가를 클릭합니다.
- 피드 이름 필드에 피드 이름을 입력합니다 (예:
Jamf Pro Context logs
). - 소스 유형으로 Amazon S3 V2를 선택합니다.
- 로그 유형으로 Jamf Pro 컨텍스트를 선택합니다.
- 다음을 클릭합니다.
- 다음 입력 파라미터의 값을 지정합니다.
- S3 URI: 버킷 URI입니다.
s3://jamfpro/jamf-pro/context/
jamfpro
을 버킷의 실제 이름으로 바꿉니다.
- 소스 삭제 옵션: 환경설정에 따라 삭제 옵션을 선택합니다.
- 최대 파일 기간: 지난 일수 동안 수정된 파일을 포함합니다. 기본값은 180일입니다.
- 액세스 키 ID: s3 버킷에 액세스할 수 있는 사용자 액세스 키입니다.
- 보안 비밀 액세스 키: s3 버킷에 액세스할 수 있는 사용자 보안 비밀 키입니다.
- 애셋 네임스페이스: 애셋 네임스페이스입니다.
- 수집 라벨: 이 피드의 이벤트에 적용할 라벨입니다.
- S3 URI: 버킷 URI입니다.
- 다음을 클릭합니다.
- 확정 화면에서 새 피드 구성을 검토하고 제출을 클릭합니다.
도움이 더 필요하신가요? 커뮤니티 회원 및 Google SecOps 전문가로부터 답변을 받으세요.