d3e0769799
- Restructure launchers: Start-IntuneToolkit.ps1 moves to repo root; Start-HeadlessIntune.ps1 moves to Scripts/; TUI helper moves to Scripts/Private/ - Add AGENTS.md with project architecture, entry points, and security notes - Add CIS M365 baseline assets (CISM365-v7, M365-CIS-Rapid) and reporting scripts - Add Python reporting utilities (Export-SettingsReport, Export-AssignmentReport, Export-ObjectInventoryReport) and CA wizard helpers - Update Deploy-IntuneBaseline.ps1 with Merge conflict resolution, ReportPath, and optimized group loading - Update Initialize-IntuneAuth.ps1 with -RotateSecret and configurable secret expiry - Update Extensions for Settings Catalog definition auto-export - Update README with v4.1.0, new entry points and script catalog - Bump VERSION to 4.1.0 - Harden .gitignore against .DS_Store, __pycache__, .venv-pdf/, local exports, Settings.json and IntuneManagement.log
779 lines
36 KiB
Python
779 lines
36 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Convert CIS M365 v7.0.0 draft PDF to YAML baseline manifest.
|
|
Called by ConvertFrom-CISPDF.ps1
|
|
"""
|
|
import sys
|
|
import re
|
|
from pathlib import Path
|
|
from pypdf import PdfReader
|
|
|
|
|
|
def parse_profiles(pa_text: str | None) -> set[tuple[str, str]]:
|
|
"""Extract (level, license) tuples from Profile Applicability text.
|
|
Example: '• E3 Level 1 • E5 Level 2' → {('L1','E3'), ('L2','E5')}
|
|
"""
|
|
if not pa_text:
|
|
return set()
|
|
profiles = set()
|
|
# Split by bullet to avoid cross-bullet matching
|
|
bullets = re.split(r'\s*•\s*', pa_text)
|
|
for bullet in bullets:
|
|
bullet = bullet.strip()
|
|
if not bullet:
|
|
continue
|
|
# Look for patterns like "E3 Level 1" or "Level 1 E3" within a single bullet
|
|
m = re.search(r'\b(E3|E5)\b.*\bLevel\s+(1|2)\b', bullet, re.IGNORECASE)
|
|
if not m:
|
|
m = re.search(r'\bLevel\s+(1|2)\b.*\b(E3|E5)\b', bullet, re.IGNORECASE)
|
|
if m:
|
|
level = f"L{m.group(1)}"
|
|
license = m.group(2).upper()
|
|
profiles.add((level, license))
|
|
else:
|
|
level = f"L{m.group(2)}"
|
|
license = m.group(1).upper()
|
|
profiles.add((level, license))
|
|
return profiles
|
|
|
|
|
|
def format_profiles(profiles: set[tuple[str, str]]) -> str:
|
|
"""Format profile set as compact badge string."""
|
|
if not profiles:
|
|
return ""
|
|
return "[" + ", ".join(f"{lvl}·{lic}" for lvl, lic in sorted(profiles)) + "]"
|
|
|
|
|
|
def matches_filter(profiles: set[tuple[str, str]], level_filter: str, license_filter: str) -> bool:
|
|
"""Check if a control's profiles match the requested level/license filters.
|
|
A control matches if at least one of its (level, license) tuples matches both filters.
|
|
"""
|
|
if not profiles:
|
|
return True # If we can't parse profiles, include by default
|
|
for lvl, lic in profiles:
|
|
level_ok = level_filter == 'Both' or level_filter == lvl
|
|
license_ok = license_filter == 'Both' or license_filter == lic
|
|
if level_ok and license_ok:
|
|
return True
|
|
return False
|
|
|
|
|
|
def parse_pdf(pdf_path: str) -> list[dict]:
|
|
"""Extract and parse all recommendations from the PDF."""
|
|
reader = PdfReader(pdf_path)
|
|
full_text = ""
|
|
for page in reader.pages:
|
|
full_text += "\n" + (page.extract_text() or "")
|
|
|
|
m = re.search(r'Profile Applicability:\s*\n\s*•\s*E3', full_text)
|
|
content_start = m.start() if m else 0
|
|
content = full_text[content_start:]
|
|
content = re.sub(r'\nPage \d+\s*\n', '\n', content)
|
|
|
|
section_headers = {
|
|
'Overview', 'Groups', 'Devices', 'Enterprise apps', 'External Identities',
|
|
'User experiences', 'Authentication Methods', 'Password reset', 'Identity Protection',
|
|
'Conditional Access', 'Protection', 'Hybrid management', 'Audit', 'Mail flow',
|
|
'Roles', 'Mobile Device Management', 'Application Permissions', 'Settings',
|
|
'Teams & groups', 'Users', 'External sharing', 'Guest access', 'Device access',
|
|
'User risk', 'Sign-in risk', 'Access reviews', 'Privileged Identity Management',
|
|
'Administration center', 'Email and collaboration', 'Tenant settings',
|
|
'Meetings', 'Messaging', 'Teams and channels', 'App permissions',
|
|
'External access', 'Data sharing', 'File sharing', 'Site settings',
|
|
'Service principals', 'Workspaces', 'External domains', 'External emails',
|
|
'Meeting policies', 'Calling policies', 'Teams policies', 'Channel policies',
|
|
'App setup policies', 'Permission policies', 'Update policies',
|
|
'Compliance policies', 'Retention policies', 'Sensitivity labels',
|
|
'Data loss prevention', 'Information barriers', 'Communication compliance',
|
|
'Insider risk management', 'Records management', 'eDiscovery',
|
|
'Customer Lockbox', 'Audit log', 'Reports', 'Alerts',
|
|
'Anti-spam', 'Anti-malware', 'Anti-phishing', 'Safe Attachments',
|
|
'Safe Links', 'Outbound spam', 'Connection filter', 'Mail flow rules',
|
|
'Transport rules', 'Journal rules', 'Data connectors',
|
|
'Sensitivity label policies', 'Auto-labeling policies',
|
|
'Information protection', 'Data governance', 'Compliance Manager',
|
|
'Service assurance', 'Health', 'Message center', 'Adoption Score',
|
|
'Usage reports', 'Productivity Score', 'Org settings',
|
|
'Security & Privacy', 'Organization profile', 'Partner relationships',
|
|
'Billing', 'Purchase services', 'Subscriptions', 'Licenses',
|
|
'Payment methods', 'Billing notifications', 'Invoice',
|
|
'Active users', 'Deleted users', 'Guest users',
|
|
'Contacts', 'Sign-in options',
|
|
'Custom domain names', 'DNS records', 'Domain settings',
|
|
'Shared mailboxes', 'Resource mailboxes', 'Distribution groups',
|
|
'Dynamic distribution groups', 'Mail-enabled security groups',
|
|
'Office 365 groups', 'Security groups', 'Mail contacts',
|
|
'Migration', 'Data migration', 'IMAP migration',
|
|
'Cutover migration', 'Staged migration', 'Minimal hybrid',
|
|
'Express migration', 'Cross-tenant migration',
|
|
'Setup', 'Connectors',
|
|
'Azure AD', 'Support',
|
|
'Training', 'Policies', 'Resources', 'Mail',
|
|
'Sites', 'Apps', 'Power Platform',
|
|
'Dynamics 365', 'Azure', 'Microsoft 365',
|
|
'Intune', 'Entra', 'Exchange', 'SharePoint',
|
|
'OneDrive', 'Power BI', 'Power Apps',
|
|
'Power Automate', 'Power Virtual Agents', 'Copilot',
|
|
}
|
|
|
|
pa_positions = [m.start() for m in re.finditer(r'Profile Applicability:', content)]
|
|
recommendations = []
|
|
|
|
for i, pa_pos in enumerate(pa_positions):
|
|
window_start = max(0, pa_pos - 800)
|
|
window = content[window_start:pa_pos]
|
|
|
|
title_match = None
|
|
for m in re.finditer(r'(\d+\.\d+\.\d+\.\d+)\s+(.+?)\s*\((Automated|Manual)\)', window, re.DOTALL):
|
|
title_match = m
|
|
if not title_match:
|
|
for m in re.finditer(r'(\d+\.\d+\.\d+)\s+(.+?)\s*\((Automated|Manual)\)', window, re.DOTALL):
|
|
title_match = m
|
|
|
|
if not title_match:
|
|
continue
|
|
|
|
control_num = title_match.group(1)
|
|
title = title_match.group(2).replace('\n', ' ').strip()
|
|
title = re.sub(r'\s+', ' ', title)
|
|
status = title_match.group(3)
|
|
|
|
if title in section_headers:
|
|
continue
|
|
|
|
rec_start = title_match.start() + window_start
|
|
rec_end = pa_positions[i + 1] if i + 1 < len(pa_positions) else len(content)
|
|
chunk = content[rec_start:rec_end]
|
|
|
|
def extract_field(field_name: str, chunk_text: str) -> str | None:
|
|
pattern = re.compile(
|
|
re.escape(field_name) + r':\s*\n?\s*(.*?)(?=\n\s*[A-Z][a-zA-Z\s]+:\s*\n|\Z)',
|
|
re.DOTALL
|
|
)
|
|
m = pattern.search(chunk_text)
|
|
if m:
|
|
val = m.group(1).strip()
|
|
val = re.sub(r'\s+', ' ', val)
|
|
return val
|
|
return None
|
|
|
|
rec = {
|
|
'control': control_num,
|
|
'title': title,
|
|
'status': status,
|
|
'profile_applicability': extract_field('Profile Applicability', chunk),
|
|
'description': extract_field('Description', chunk),
|
|
'rationale': extract_field('Rationale', chunk),
|
|
'impact': extract_field('Impact', chunk),
|
|
'default_value': extract_field('Default Value', chunk),
|
|
}
|
|
|
|
rem_match = re.search(r'Remediation:\s*(.*?)(?=Audit:|Default Value:|References:|CIS Controls:|\Z)', chunk, re.DOTALL)
|
|
if rem_match:
|
|
rec['remediation'] = re.sub(r'\s+', ' ', rem_match.group(1))[:1000]
|
|
|
|
audit_match = re.search(r'Audit:\s*(.*?)(?=Remediation:|Default Value:|References:|CIS Controls:|\Z)', chunk, re.DOTALL)
|
|
if audit_match:
|
|
rec['audit'] = re.sub(r'\s+', ' ', audit_match.group(1))[:1000]
|
|
|
|
recommendations.append(rec)
|
|
|
|
seen = set()
|
|
unique = []
|
|
for r in recommendations:
|
|
if r['control'] not in seen:
|
|
seen.add(r['control'])
|
|
unique.append(r)
|
|
|
|
return unique
|
|
|
|
|
|
def generate_yaml(recommendations: list[dict], prefix: str, level_filter: str = 'Both', license_filter: str = 'Both') -> str:
|
|
"""Generate YAML baseline from parsed recommendations."""
|
|
lines = []
|
|
lines.append("# =====================================================================")
|
|
lines.append("# CIS Microsoft 365 Foundations Benchmark v7.0.0 (Draft)")
|
|
lines.append("# GENERATED from PDF — review before deploying")
|
|
lines.append("# =====================================================================")
|
|
lines.append("")
|
|
lines.append("baseline:")
|
|
lines.append(f' name: CIS-M365-v7-Generated')
|
|
lines.append(' conflictResolution: Skip')
|
|
lines.append(' whatIf: false')
|
|
lines.append("")
|
|
lines.append(' tenantMutation:')
|
|
lines.append(f' prefix: "{prefix}"')
|
|
lines.append("")
|
|
lines.append(' groups:')
|
|
lines.append(' - displayName: "CIS-BreakGlass"')
|
|
lines.append(' mailNickname: "CISBreakGlass"')
|
|
lines.append(' securityEnabled: true')
|
|
lines.append(' - displayName: "CIS-Pilot-Users"')
|
|
lines.append(' mailNickname: "CISPilotUsers"')
|
|
lines.append(' securityEnabled: true')
|
|
lines.append("")
|
|
lines.append(' tenantConfig:')
|
|
|
|
section_names = {
|
|
'1': 'adminCenter',
|
|
'2': 'defender',
|
|
'3': 'purview',
|
|
'5': 'entraId',
|
|
'6': 'exchange',
|
|
'7': 'sharePoint',
|
|
'8': 'teams',
|
|
'9': 'powerBI',
|
|
}
|
|
|
|
# =====================================================================
|
|
# COMPREHENSIVE CONTROL MAPPINGS
|
|
# =====================================================================
|
|
|
|
# Simple scalar/boolean mappings: control -> (yaml_section, yaml_key, value)
|
|
simple_mappings = {
|
|
# --- Section 1: Admin Center ---
|
|
'1.3.1': ('adminCenter', 'passwordExpiration', 'NeverExpire'),
|
|
'1.3.2': ('adminCenter', 'idleSessionTimeoutHours', 3),
|
|
'1.3.4': ('adminCenter', 'restrictUserOwnedApps', True),
|
|
'1.3.5': ('adminCenter', 'formsPhishingProtection', True),
|
|
'1.3.6': ('adminCenter', 'customerLockbox', True),
|
|
'1.3.7': ('adminCenter', 'restrictThirdPartyStorage', True),
|
|
'1.3.9': ('adminCenter', 'restrictSharedBookings', True),
|
|
'1.3.3': ('adminCenter', 'externalCalendarSharing', 'Disabled'),
|
|
|
|
# --- Section 5: Entra ID ---
|
|
'5.1.2.2': ('entraId', 'blockUserConsent', True),
|
|
'5.1.2.3': ('entraId', 'blockTenantCreation', True),
|
|
'5.1.2.4': ('entraId', 'restrictAdminCenterAccess', True),
|
|
'5.1.2.6': ('entraId', 'disableLinkedIn', True),
|
|
'5.1.3.1': ('entraId', 'blockSecurityGroupCreation', True),
|
|
'5.1.3.4': ('entraId', 'blockM365GroupCreation', True),
|
|
'5.1.4.1': ('entraId', 'restrictDeviceJoin', True),
|
|
'5.1.4.2': ('entraId', 'maxDevicesPerUser', 5),
|
|
'5.1.4.3': ('entraId', 'gaLocalAdminDisabled', True),
|
|
'5.1.4.4': ('entraId', 'limitLocalAdminAssignment', True),
|
|
'5.1.4.5': ('entraId', 'enableLAPS', True),
|
|
'5.1.4.6': ('entraId', 'restrictBitLockerRecovery', True),
|
|
'5.1.5.1': ('entraId', 'blockUserConsent', True),
|
|
'5.1.5.2': ('entraId', 'enableAdminConsentWorkflow', True),
|
|
'5.1.5.3': ('entraId', 'blockPasswordCredentials', True),
|
|
'5.1.5.4': ('entraId', 'maxPasswordLifetimeDays', 180),
|
|
'5.1.5.5': ('entraId', 'systemGeneratedPasswords', True),
|
|
'5.1.5.6': ('entraId', 'maxCertificateLifetimeDays', 180),
|
|
'5.1.6.1': ('entraId', 'restrictCollaborationDomains', True),
|
|
'5.1.6.2': ('entraId', 'restrictGuestAccess', True),
|
|
'5.1.6.3': ('entraId', 'limitGuestInvitations', True),
|
|
'5.1.8.1': ('entraId', 'enablePasswordHashSync', True),
|
|
'5.2.3.1': ('entraId', 'authenticatorNumberMatching', True),
|
|
'5.2.3.4': ('entraId', 'mfaCapableUsers', True),
|
|
'5.2.3.5': ('entraId', 'disableWeakAuthMethods', True),
|
|
'5.2.3.6': ('entraId', 'systemPreferredMFA', True),
|
|
'5.2.3.7': ('entraId', 'disableEmailOTP', True),
|
|
'5.2.3.8': ('entraId', 'lockoutThreshold', 10),
|
|
'5.2.3.9': ('entraId', 'lockoutDurationSeconds', 60),
|
|
'5.2.3.10': ('entraId', 'disableAuthenticatorCompanionApps', True),
|
|
'5.3.1': ('entraId', 'pimRoleActivationRequired', True),
|
|
'5.3.2': ('entraId', 'accessReviewsForGuests', True),
|
|
'5.3.3': ('entraId', 'accessReviewsForPrivilegedRoles', True),
|
|
'5.3.4': ('entraId', 'requireApprovalForGAActivation', True),
|
|
'5.3.5': ('entraId', 'requireApprovalForPRAActivation', True),
|
|
|
|
# --- Section 6: Exchange ---
|
|
'6.1.1': ('exchange', 'enableMailboxAuditOrgWide', True),
|
|
'6.1.2': ('exchange', 'configureMailboxAuditActions', True),
|
|
'6.1.3': ('exchange', 'disableAuditBypass', True),
|
|
'6.2.1': ('exchange', 'blockExternalForwarding', True),
|
|
'6.2.2': ('exchange', 'noDomainWhitelistTransportRules', True),
|
|
'6.2.3': ('exchange', 'enableExternalSenderBanner', True),
|
|
'6.3.1': ('exchange', 'blockOutlookAddIns', True),
|
|
'6.3.2': ('exchange', 'disablePersonalEmailAccounts', True),
|
|
'6.5.1': ('exchange', 'enableModernAuthExchange', True),
|
|
'6.5.2': ('exchange', 'enableMailTips', True),
|
|
'6.5.3': ('exchange', 'restrictAdditionalStorageProviders', True),
|
|
'6.5.4': ('exchange', 'disableSMTPAuth', True),
|
|
'6.5.5': ('exchange', 'rejectDirectSend', True),
|
|
'1.2.2': ('exchange', 'blockSharedMailboxSignIn', True),
|
|
'2.1.12': ('exchange', 'connectionFilterIPAllowListEmpty', True),
|
|
'2.1.13': ('exchange', 'connectionFilterSafeListOff', True),
|
|
'2.1.14': ('exchange', 'inboundAntiSpamNoAllowedDomains', True),
|
|
'2.1.15': ('exchange', 'outboundAntiSpamLimits', True),
|
|
|
|
# --- Section 7: SharePoint ---
|
|
'7.2.1': ('sharePoint', 'requireModernAuthSharePoint', True),
|
|
'7.2.2': ('sharePoint', 'enableAADB2BIntegration', True),
|
|
'7.2.3': ('sharePoint', 'sharePointExternalSharing', 'Disabled'),
|
|
'7.2.4': ('sharePoint', 'oneDriveExternalSharing', 'Disabled'),
|
|
'7.2.5': ('sharePoint', 'preventGuestResharing', True),
|
|
'7.2.6': ('sharePoint', 'restrictSharePointExternalSharing', True),
|
|
'7.2.7': ('sharePoint', 'restrictLinkSharing', True),
|
|
'7.2.8': ('sharePoint', 'restrictSharingBySecurityGroup', True),
|
|
'7.2.9': ('sharePoint', 'guestAccessExpirationDays', 30),
|
|
'7.2.10': ('sharePoint', 'restrictReauthenticationVerificationCode', True),
|
|
'7.2.11': ('sharePoint', 'defaultSharingLinkPermission', 'View'),
|
|
'7.3.1': ('sharePoint', 'disallowInfectedFileDownload', True),
|
|
|
|
# --- Section 8: Teams ---
|
|
'8.1.1': ('teams', 'restrictExternalFileSharing', True),
|
|
'8.1.2': ('teams', 'blockChannelEmail', True),
|
|
'8.2.1': ('teams', 'restrictExternalDomains', True),
|
|
'8.2.2': ('teams', 'disableUnmanagedUserCommunication', True),
|
|
'8.2.3': ('teams', 'blockExternalUserInitiation', True),
|
|
'8.2.4': ('teams', 'blockTrialTenantCommunication', True),
|
|
'8.5.1': ('teams', 'allowAnonymousUsersToJoinMeeting', False),
|
|
'8.5.2': ('teams', 'allowAnonymousUsersToStartMeeting', False),
|
|
'8.5.3': ('teams', 'orgOnlyBypassLobby', True),
|
|
'8.5.4': ('teams', 'dialInCantBypassLobby', True),
|
|
'8.5.5': ('teams', 'noAnonymousMeetingChat', True),
|
|
'8.5.6': ('teams', 'onlyOrganizersCanPresent', True),
|
|
'8.5.7': ('teams', 'noExternalControl', True),
|
|
'8.5.8': ('teams', 'externalMeetingChatOff', True),
|
|
'8.5.9': ('teams', 'meetingRecordingOffByDefault', True),
|
|
'8.6.1': ('teams', 'enableSecurityConcernsReporting', True),
|
|
|
|
# --- Section 9: Power BI ---
|
|
'9.1.1': ('powerBI', 'restrictGuestAccess', True),
|
|
'9.1.2': ('powerBI', 'restrictExternalInvitations', True),
|
|
'9.1.3': ('powerBI', 'restrictGuestContentAccess', True),
|
|
'9.1.4': ('powerBI', 'restrictPublishToWeb', True),
|
|
'9.1.5': ('powerBI', 'disableRPythonVisuals', True),
|
|
'9.1.6': ('powerBI', 'enableSensitivityLabels', True),
|
|
'9.1.7': ('powerBI', 'restrictShareableLinks', True),
|
|
'9.1.8': ('powerBI', 'restrictExternalDataSharing', True),
|
|
'9.1.9': ('powerBI', 'blockResourceKeyAuth', True),
|
|
'9.1.10': ('powerBI', 'restrictServicePrincipalAPIAccess', True),
|
|
'9.1.11': ('powerBI', 'blockServicePrincipalProfiles', True),
|
|
'9.1.12': ('powerBI', 'restrictServicePrincipalWorkspaceCreation', True),
|
|
|
|
# --- Section 3: Purview ---
|
|
'3.1.1': ('purview', 'enableAuditLogSearch', True),
|
|
}
|
|
|
|
# Defender policy mappings
|
|
defender_policies = {
|
|
'2.1.1': ('safeLinks', {
|
|
'name': 'SafeLinks-Default',
|
|
'enabled': True,
|
|
'trackClicks': True,
|
|
'allowClickThrough': False,
|
|
'scanUrls': True,
|
|
'enableForInternalSenders': True,
|
|
}),
|
|
'2.1.2': ('antiMalware', {
|
|
'name': 'AntiMalware-Default',
|
|
'enabled': True,
|
|
'enableInternalNotifications': True,
|
|
'fileTypes': ['ace', 'ani', 'app', 'docm', 'exe', 'jar', 'jnlp', 'msi', 'ps1', 'scr', 'vbs', 'wsf'],
|
|
}),
|
|
'2.1.3': ('antiMalware', {
|
|
'name': 'AntiMalware-InternalNotify',
|
|
'enabled': True,
|
|
'enableInternalNotifications': True,
|
|
}),
|
|
'2.1.4': ('safeAttachments', {
|
|
'name': 'SafeAttachments-Default',
|
|
'enabled': True,
|
|
'action': 'Block',
|
|
'quarantineMessages': True,
|
|
}),
|
|
'2.1.5': ('safeAttachments', {
|
|
'name': 'SafeAttachments-SPO-Teams',
|
|
'enabled': True,
|
|
'action': 'Block',
|
|
'enableForSharePoint': True,
|
|
'enableForTeams': True,
|
|
}),
|
|
'2.1.6': ('antiSpam', {
|
|
'name': 'AntiSpam-Notify-Admins',
|
|
'enabled': True,
|
|
'notifyAdmins': True,
|
|
}),
|
|
'2.1.7': ('antiPhish', {
|
|
'name': 'AntiPhish-Default',
|
|
'enabled': True,
|
|
'enableMailboxIntelligence': True,
|
|
'enableSpoofIntelligence': True,
|
|
'mailboxIntelligenceProtectionAction': 'Quarantine',
|
|
}),
|
|
'2.1.11': ('antiMalware', {
|
|
'name': 'AntiMalware-Comprehensive',
|
|
'enabled': True,
|
|
'enableFileFilter': True,
|
|
}),
|
|
'2.4.1': ('priorityAccount', {'enabled': True}),
|
|
'2.4.2': ('priorityAccount', {'strictProtection': True}),
|
|
'2.4.4': ('zap', {'enabledForTeams': True}),
|
|
}
|
|
|
|
# Draft YAML blocks for tenant-specific controls (commented out)
|
|
draft_blocks = {
|
|
'1.1.3': [
|
|
" # ASSESSMENT-ONLY: Report current global admin count; cannot auto-remediate",
|
|
" # assessment:",
|
|
" # control: \"1.1.3\"",
|
|
" # name: \"GlobalAdminCount\"",
|
|
" # minAdmins: 2",
|
|
" # maxAdmins: 4",
|
|
],
|
|
'1.1.4': [
|
|
" # ASSESSMENT-ONLY: Report admin license footprint; cannot auto-remediate",
|
|
" # assessment:",
|
|
" # control: \"1.1.4\"",
|
|
" # name: \"AdminLicenseFootprint\"",
|
|
" # allowedSkus: [\"AAD_PREMIUM_P2\", \"ENTERPRISEPACK\", \"SPE_E5\"]",
|
|
],
|
|
'1.2.1': [
|
|
" # ASSESSMENT-ONLY: Review public groups; cannot auto-remediate",
|
|
" # assessment:",
|
|
" # control: \"1.2.1\"",
|
|
" # name: \"PublicGroupReview\"",
|
|
" # visibilityFilter: \"Public\"",
|
|
],
|
|
'3.2.1': [
|
|
" # DRAFT: Uncomment and customize DLP policies for your environment",
|
|
" # dlpPolicies:",
|
|
" # - name: \"CIS-DLP-Financial-Data\"",
|
|
" # enabled: true",
|
|
" # mode: \"Enable\"",
|
|
" # locations:",
|
|
" # - type: \"Exchange\"",
|
|
" # - type: \"SharePoint\"",
|
|
" # - type: \"OneDrive\"",
|
|
" # rules:",
|
|
" # - name: \"Detect-Credit-Cards\"",
|
|
" # sensitiveInfoTypes: [\"Credit Card Number\"]",
|
|
" # actions: [\"BlockWithOverride\"]",
|
|
" # userNotification: true",
|
|
" # - name: \"CIS-DLP-PII\"",
|
|
" # enabled: true",
|
|
" # mode: \"Enable\"",
|
|
" # locations:",
|
|
" # - type: \"TeamsChat\"",
|
|
" # - type: \"TeamsChannel\"",
|
|
" # rules:",
|
|
" # - name: \"Detect-SSN\"",
|
|
" # sensitiveInfoTypes: [\"U.S. Social Security Number\"]",
|
|
" # actions: [\"BlockWithOverride\"]",
|
|
" # userNotification: true",
|
|
],
|
|
'3.2.2': [
|
|
" # DRAFT: Uncomment and customize Teams DLP policy",
|
|
" # dlpPolicies:",
|
|
" # - name: \"CIS-DLP-Teams\"",
|
|
" # enabled: true",
|
|
" # mode: \"Enable\"",
|
|
" # locations:",
|
|
" # - type: \"TeamsChat\"",
|
|
" # - type: \"TeamsChannel\"",
|
|
" # rules:",
|
|
" # - name: \"Teams-Detect-PII\"",
|
|
" # sensitiveInfoTypes: [\"Credit Card Number\", \"U.S. Social Security Number\"]",
|
|
" # actions: [\"BlockWithOverride\"]",
|
|
" # userNotification: true",
|
|
],
|
|
'3.2.3': [
|
|
" # DRAFT: Uncomment and customize Copilot DLP policy",
|
|
" # dlpPolicies:",
|
|
" # - name: \"CIS-DLP-Copilot\"",
|
|
" # enabled: true",
|
|
" # mode: \"Enable\"",
|
|
" # locations:",
|
|
" # - type: \"TeamsChat\"",
|
|
" # - type: \"TeamsChannel\"",
|
|
" # rules:",
|
|
" # - name: \"Copilot-Detect-Sensitive\"",
|
|
" # sensitiveInfoTypes: [\"Credit Card Number\", \"U.S. Social Security Number\"]",
|
|
" # actions: [\"BlockWithOverride\"]",
|
|
" # userNotification: true",
|
|
],
|
|
'3.3.1': [
|
|
" # DRAFT: Uncomment and customize sensitivity labels for your organization",
|
|
" # sensitivityLabels:",
|
|
" # - name: \"Internal\"",
|
|
" # displayName: \"Internal\"",
|
|
" # priority: 1",
|
|
" # enabled: true",
|
|
" # labelAction: \"Encrypt\"",
|
|
" # - name: \"Confidential\"",
|
|
" # displayName: \"Confidential\"",
|
|
" # priority: 2",
|
|
" # enabled: true",
|
|
" # labelAction: \"Encrypt\"",
|
|
" # sensitivityLabelPolicies:",
|
|
" # - name: \"CIS-Label-Policy-Default\"",
|
|
" # enabled: true",
|
|
" # labels: [\"Internal\", \"Confidential\"]",
|
|
" # defaultLabel: \"Internal\"",
|
|
],
|
|
}
|
|
|
|
def format_val(val):
|
|
if isinstance(val, str):
|
|
return f'"{val}"'
|
|
elif isinstance(val, bool):
|
|
return str(val).lower()
|
|
elif isinstance(val, list):
|
|
return '[' + ', '.join(f'"{v}"' for v in val) + ']'
|
|
return str(val)
|
|
|
|
def write_simple_section(sec_num, sec_name, sec_recs):
|
|
if not sec_recs:
|
|
return
|
|
lines.append("")
|
|
lines.append(f" # ===============================================================")
|
|
lines.append(f" # Section {sec_num}: {sec_name}")
|
|
lines.append(f" # ===============================================================")
|
|
lines.append(f" {sec_name}:")
|
|
|
|
for r in sec_recs:
|
|
ctrl = r['control']
|
|
title = r['title']
|
|
status = r['status']
|
|
profiles = parse_profiles(r.get('profile_applicability'))
|
|
profile_badge = format_profiles(profiles)
|
|
|
|
# Filter by level/license
|
|
if not matches_filter(profiles, level_filter, license_filter):
|
|
continue
|
|
|
|
# Skip CA policies — they are handled in the conditionalAccess section
|
|
if ctrl.startswith('5.2.2.'):
|
|
continue
|
|
# Skip on-prem AD password protection — hybrid only
|
|
if ctrl == '5.2.3.3':
|
|
lines.append(f" # {ctrl} {profile_badge}({status}): {title}")
|
|
lines.append(f" # NOTE: Hybrid-only control — requires on-premises Active Directory")
|
|
continue
|
|
# Banned passwords — add inline with external file support
|
|
if ctrl == '5.2.3.2':
|
|
lines.append(f" # {ctrl} {profile_badge}: {title}")
|
|
lines.append(f" # Option A: Inline list")
|
|
lines.append(f" bannedPasswords:")
|
|
lines.append(f" - \"Contoso\"")
|
|
lines.append(f" - \"Password\"")
|
|
lines.append(f" - \"Welcome\"")
|
|
lines.append(f" - \"Admin\"")
|
|
lines.append(f" - \"Login\"")
|
|
lines.append(f" - \"Microsoft\"")
|
|
lines.append(f" - \"Office365\"")
|
|
lines.append(f" # Option B: External file (one password per line)")
|
|
lines.append(f" # bannedPasswordsFile: \"./banned-passwords.txt\"")
|
|
continue
|
|
|
|
if status == 'Manual':
|
|
lines.append(f" # {ctrl} {profile_badge}(Manual): {title}")
|
|
rem = r.get('remediation', '')
|
|
hint = rem[:120] + '...' if len(rem) > 120 else rem
|
|
if hint:
|
|
lines.append(f" # HINT: {hint}")
|
|
lines.append(f" # TODO: Implement manually per PDF instructions")
|
|
continue
|
|
|
|
if ctrl in simple_mappings:
|
|
sec, key, val = simple_mappings[ctrl]
|
|
lines.append(f" # {ctrl} {profile_badge}: {title}")
|
|
lines.append(f" {key}: {format_val(val)}")
|
|
elif ctrl in draft_blocks:
|
|
lines.append(f" # {ctrl} {profile_badge}({status}): {title}")
|
|
for line in draft_blocks[ctrl]:
|
|
lines.append(line)
|
|
else:
|
|
lines.append(f" # {ctrl} {profile_badge}({status}): {title}")
|
|
lines.append(f" # TODO: Map this control to YAML — see PDF for details")
|
|
|
|
# Write non-defender, non-CA sections
|
|
for sec_num in ['1', '5', '6', '7', '8', '9', '3']:
|
|
sec_name = section_names[sec_num]
|
|
sec_recs = [r for r in recommendations if r['control'].split('.')[0] == sec_num]
|
|
write_simple_section(sec_num, sec_name, sec_recs)
|
|
|
|
# Defender section (with proper policy structures)
|
|
def_recs = [r for r in recommendations if r['control'].split('.')[0] == '2']
|
|
if def_recs:
|
|
lines.append("")
|
|
lines.append(" # ===============================================================")
|
|
lines.append(" # Section 2: Defender for Office 365")
|
|
lines.append(" # ===============================================================")
|
|
lines.append(" defender:")
|
|
|
|
for r in def_recs:
|
|
ctrl = r['control']
|
|
title = r['title']
|
|
status = r['status']
|
|
profiles = parse_profiles(r.get('profile_applicability'))
|
|
profile_badge = format_profiles(profiles)
|
|
|
|
if not matches_filter(profiles, level_filter, license_filter):
|
|
continue
|
|
|
|
if status == 'Manual':
|
|
lines.append(f" # {ctrl} {profile_badge}(Manual): {title}")
|
|
continue
|
|
|
|
if ctrl in defender_policies:
|
|
policy_type, policy_def = defender_policies[ctrl]
|
|
lines.append(f" # {ctrl} {profile_badge}: {title}")
|
|
lines.append(f" {policy_type}:")
|
|
for k, v in policy_def.items():
|
|
lines.append(f" {k}: {format_val(v)}")
|
|
elif ctrl in ['2.1.8', '2.1.9', '2.1.10']:
|
|
lines.append(f" # {ctrl} {profile_badge}({status}): {title}")
|
|
lines.append(f" # NOTE: DNS-level control — configure via DNS provider, not M365 tenant")
|
|
elif ctrl in simple_mappings:
|
|
sec, key, val = simple_mappings[ctrl]
|
|
lines.append(f" # {ctrl} {profile_badge}: {title}")
|
|
lines.append(f" {key}: {format_val(val)}")
|
|
else:
|
|
lines.append(f" # {ctrl} {profile_badge}({status}): {title}")
|
|
lines.append(f" # TODO: Map this control to YAML — see PDF for details")
|
|
|
|
# Conditional Access section
|
|
ca_recs = [r for r in recommendations if r['control'].startswith('5.2.2.')]
|
|
if ca_recs:
|
|
lines.append("")
|
|
lines.append(" # ===============================================================")
|
|
lines.append(" # Section 5.2.2: Conditional Access")
|
|
lines.append(" # ===============================================================")
|
|
lines.append(" conditionalAccess:")
|
|
lines.append(" reportOnly: true")
|
|
lines.append(" breakGlassGroup: \"CIS-BreakGlass\"")
|
|
lines.append(" policies:")
|
|
|
|
for r in ca_recs:
|
|
ctrl = r['control']
|
|
title = r['title']
|
|
status = r['status']
|
|
profiles = parse_profiles(r.get('profile_applicability'))
|
|
profile_badge = format_profiles(profiles)
|
|
|
|
if not matches_filter(profiles, level_filter, license_filter):
|
|
continue
|
|
|
|
if status == 'Manual':
|
|
lines.append(f" # {ctrl} {profile_badge}(Manual): {title}")
|
|
continue
|
|
|
|
name = re.sub(r'[^a-zA-Z0-9\s]', '', title)
|
|
name = re.sub(r'\s+', '-', name)
|
|
name = re.sub(r'-+', '-', name)
|
|
name = name[:55].strip('-')
|
|
|
|
lines.append(f" - name: \"{name}\"")
|
|
lines.append(f" cisControl: \"{ctrl}\"")
|
|
lines.append(f" description: \"{title}\"")
|
|
lines.append(f" state: enabledForReportingButNotEnforced")
|
|
lines.append(f" conditions:")
|
|
lines.append(f" applications:")
|
|
|
|
t = title.lower()
|
|
if 'intune enrollment' in t:
|
|
lines.append(f" includeApplications: [\"0000000a-0000-0000-c000-000000000000\"]")
|
|
elif 'register security' in t:
|
|
lines.append(f" includeUserActions: [\"urn:user:registersecurityinfo\"]")
|
|
else:
|
|
lines.append(f" includeApplications: [\"All\"]")
|
|
|
|
lines.append(f" users:")
|
|
|
|
if 'admin' in t or 'administrator' in t:
|
|
lines.append(f" includeRoles:")
|
|
lines.append(f" - \"Global Administrator\"")
|
|
lines.append(f" - \"Privileged Role Administrator\"")
|
|
lines.append(f" - \"Security Administrator\"")
|
|
lines.append(f" - \"Exchange Administrator\"")
|
|
lines.append(f" - \"SharePoint Administrator\"")
|
|
lines.append(f" - \"Conditional Access Administrator\"")
|
|
lines.append(f" - \"Application Administrator\"")
|
|
lines.append(f" - \"Cloud Application Administrator\"")
|
|
lines.append(f" - \"User Administrator\"")
|
|
lines.append(f" - \"Helpdesk Administrator\"")
|
|
lines.append(f" - \"Billing Administrator\"")
|
|
lines.append(f" - \"Authentication Administrator\"")
|
|
lines.append(f" - \"Password Administrator\"")
|
|
lines.append(f" - \"Global Reader\"")
|
|
else:
|
|
lines.append(f" includeUsers: [\"All\"]")
|
|
|
|
if 'legacy' in t:
|
|
lines.append(f" clientAppTypes: [\"exchangeActiveSync\", \"other\"]")
|
|
elif 'device code' in t:
|
|
lines.append(f" authenticationFlows:")
|
|
lines.append(f" deviceCodeFlow:")
|
|
lines.append(f" isEnabled: true")
|
|
elif 'sign-in risk' in t or 'risk' in t:
|
|
lines.append(f" signInRiskLevels: [\"medium\", \"high\"]")
|
|
elif 'named location' in t or 'geographic' in t:
|
|
lines.append(f" # TODO: Define named locations in Entra admin center")
|
|
|
|
lines.append(f" grantControls:")
|
|
|
|
if 'block' in t and ('legacy' in t or 'device code' in t or 'risk' in t or 'authentication transfer' in t):
|
|
lines.append(f" builtInControls: [\"block\"]")
|
|
lines.append(f" operator: \"OR\"")
|
|
elif 'mfa' in t and 'phishing-resistant' in t:
|
|
lines.append(f" builtInControls: [\"authenticationStrength\"]")
|
|
lines.append(f" authenticationStrength:")
|
|
lines.append(f" id: \"00000000-0000-0000-0000-000000000004\"")
|
|
lines.append(f" operator: \"OR\"")
|
|
elif 'mfa' in t or 'multifactor' in t or 'reauthentication' in t or 're-authentication' in t:
|
|
lines.append(f" builtInControls: [\"mfa\"]")
|
|
lines.append(f" operator: \"OR\"")
|
|
elif 'managed device' in t:
|
|
lines.append(f" builtInControls: [\"compliantDevice\", \"domainJoinedDevice\"]")
|
|
lines.append(f" operator: \"OR\"")
|
|
elif 'token protection' in t:
|
|
lines.append(f" builtInControls: [\"mfa\"]")
|
|
lines.append(f" operator: \"OR\"")
|
|
lines.append(f" # TODO: Enable Token Protection via Authentication Strength policy")
|
|
else:
|
|
lines.append(f" builtInControls: [\"mfa\"]")
|
|
lines.append(f" operator: \"OR\"")
|
|
|
|
if 'sign-in frequency' in t or 'browser' in t or 'persistent' in t:
|
|
lines.append(f" sessionControls:")
|
|
lines.append(f" signInFrequency:")
|
|
lines.append(f" value: 12")
|
|
lines.append(f" type: hours")
|
|
lines.append(f" isEnabled: true")
|
|
lines.append(f" persistentBrowser:")
|
|
lines.append(f" mode: never")
|
|
lines.append(f" isEnabled: true")
|
|
|
|
return '\n'.join(lines) + '\n'
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 3:
|
|
print("Usage: _ConvertFrom-CISPDF.py <pdf_path> <output_path> [prefix] [level] [license]")
|
|
print(" level: L1 | L2 | Both (default)")
|
|
print(" license: E3 | E5 | Both (default)")
|
|
sys.exit(1)
|
|
|
|
pdf_path = sys.argv[1]
|
|
output_path = sys.argv[2]
|
|
prefix = sys.argv[3] if len(sys.argv) > 3 else "CIS-v7-"
|
|
level_filter = sys.argv[4] if len(sys.argv) > 4 else "Both"
|
|
license_filter = sys.argv[5] if len(sys.argv) > 5 else "Both"
|
|
|
|
print(f"Parsing PDF: {pdf_path}")
|
|
recommendations = parse_pdf(pdf_path)
|
|
|
|
auto = sum(1 for r in recommendations if r['status'] == 'Automated')
|
|
manual = sum(1 for r in recommendations if r['status'] == 'Manual')
|
|
print(f"Found {len(recommendations)} unique recommendations")
|
|
print(f" Automated: {auto}")
|
|
print(f" Manual: {manual}")
|
|
|
|
print(f"\nGenerating YAML (level={level_filter}, license={license_filter})...")
|
|
yaml = generate_yaml(recommendations, prefix, level_filter, license_filter)
|
|
|
|
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|
f.write(yaml)
|
|
|
|
print(f"Written: {output_path}")
|
|
print(f"Total lines: {len(yaml.splitlines())}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|