first upload
This commit is contained in:
commit
ed1485646a
3 files changed
+341
No files matched your search
@@ -0,0 +1,103 @@
|
||||
# comi-win11-max-25h2
|
||||
|
||||
Paquet de configuration WAPT **Windows 11** limitant les mises a jour de fonctionnalites Windows Update a **25H2**. Il ne desactive ni Windows Update ni les mises a jour cumulatives/de securite.
|
||||
|
||||
## Ce que configure le paquet
|
||||
|
||||
Sous `HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate` (vue 64 bits) :
|
||||
|
||||
| Valeur | Type | Donnee |
|
||||
| --- | --- | --- |
|
||||
| `ProductVersion` | `REG_SZ` | `Windows 11` |
|
||||
| `TargetReleaseVersion` | `REG_DWORD` | `1` |
|
||||
| `TargetReleaseVersionInfo` | `REG_SZ` | `25H2` |
|
||||
|
||||
- Poste Windows 11 23H2 / 24H2 : 25H2 demeure une version cible autorisee, sous reserve de compatibilite, de disponibilite et des autres politiques de mise a jour.
|
||||
- Poste Windows 11 25H2 : reste cible sur 25H2 pour les mises a jour de fonctionnalites.
|
||||
- Poste deja en 26H1/26H2 : **installation refusee** si `DisplayVersion` le revele. Ce paquet ne fait pas de retrogradation.
|
||||
- Windows 10, Windows Server, editions Home et LTSC connues : installation refusee pour eviter les effets de bord.
|
||||
|
||||
Cette politique concerne **les Feature Updates distribuees par Windows Update**. Elle ne bloque pas un autre outil, un ISO ou une mise a niveau executee manuellement. L'efficacite reelle peut aussi etre modifiee par GPO, Intune/MDM, WSUS, la fin de support de la version cible ou des politiques contradictoires. Elle ne doit pas etre consideree comme une barriere de securite absolue.
|
||||
|
||||
## Contenu
|
||||
|
||||
```
|
||||
comi-win11-max-25h2/
|
||||
setup.py
|
||||
README.md
|
||||
WAPT/
|
||||
control
|
||||
```
|
||||
|
||||
## Construire et deployer
|
||||
|
||||
1. Decompresser l'archive de sources sur un poste **WAPT Packaging** muni d'un certificat de signature autorise et de sa cle privee.
|
||||
2. Verifier / adapter le champ `maintainer` ou le prefixe `package` dans `WAPT/control`.
|
||||
3. Construire et signer via la Console WAPT (**Build and upload package**) ou en ligne de commande :
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\wapt\wapt-get.exe' build-package 'C:\waptdev\comi-win11-max-25h2'
|
||||
```
|
||||
|
||||
Pour compiler ET mettre en depot en une operation :
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\wapt\wapt-get.exe' build-upload 'C:\waptdev\comi-win11-max-25h2'
|
||||
```
|
||||
|
||||
4. Affecter `comi-win11-max-25h2` a l'hote ou au groupe voulu, et lancer l'installation depuis WAPT.
|
||||
5. Tester d'abord sur un poste Windows 11 non critique.
|
||||
|
||||
**Important :** cette archive contient les *sources*, pas un fichier `.wapt` deja signe. La signature utilise la cle de packaging de votre environnement WAPT ; aucune cle privee n'est incluse.
|
||||
|
||||
## Audit
|
||||
|
||||
- Audit WAPT journalier (`audit_schedule: 1d`) ; lance aussi apres l'upgrade / sur demande.
|
||||
- `OK` : strategie conforme et version detectee <= 25H2.
|
||||
- `ERROR` : policy differente/absente, OS incompatible ou version deja > 25H2.
|
||||
- `WARNING` : strategie conforme, mais version Windows non determinee.
|
||||
|
||||
Sur un poste WAPT :
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\wapt\wapt-get.exe' audit comi-win11-max-25h2
|
||||
Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' |
|
||||
Select-Object ProductVersion, TargetReleaseVersion, TargetReleaseVersionInfo
|
||||
```
|
||||
|
||||
La conformite des trois valeurs de Registre est un controle local : la prise en compte effective reste soumise au moteur Windows Update et a la source de mises a jour configuree.
|
||||
|
||||
## Desinstallation / retour arriere
|
||||
|
||||
Lors de la *premiere* installation, les valeurs precedemment presentes sont sauvegardees dans :
|
||||
|
||||
`C:\ProgramData\Comitari\WAPT\comi-win11-max-25h2\previous_policy.json`
|
||||
|
||||
La fonction `uninstall()` restaure les valeurs precedentes, ou retire les valeurs qui n'existaient pas. Si une valeur a change depuis l'installation sous l'effet d'un autre gestionnaire, elle est **conservee**. Les reinstallations n'ecrasent pas la sauvegarde initiale.
|
||||
|
||||
La suppression de la dependance hote, ou `wapt-get remove` seul, **ne garantit pas** selon la version WAPT l'execution du `uninstall()` personnalise. Pour annuler explicitement la strategie avant de supprimer le paquet, faire :
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\wapt\wapt-get.exe' uninstall comi-win11-max-25h2
|
||||
& 'C:\Program Files (x86)\wapt\wapt-get.exe' remove comi-win11-max-25h2
|
||||
```
|
||||
|
||||
Retirer egalement la dependance du paquet sur l'hote/groupe dans la Console WAPT ; sinon il peut etre reinstalle.
|
||||
|
||||
La fonction de desinstallation ne desactive pas Windows Update et ne supprime pas la cle WindowsUpdate parente (elle peut contenir d'autres regles).
|
||||
|
||||
## Limites
|
||||
|
||||
- Pas de retour arriere d'un Windows deja passe en version superieure.
|
||||
- Ce paquet ne force pas la migration vers 25H2 ; il fixe une **cible maximale** pour les *Feature Updates* de Windows Update.
|
||||
- Les versions de Windows sortant de maintenance peuvent necessiter une politique actualisee. Une version cible en fin de maintenance ne doit pas rester gelee indefiniment.
|
||||
- Le ciblage par Windows Update for Business est documente pour les editions Windows 11 Pro, Enterprise et Education (pas Home).
|
||||
- GPO, MDM/Intune, WSUS et autres outils peuvent surcharger la policy, ou proposer des mises a niveau par d'autres mecanismes.
|
||||
- Aucun `gpupdate`, redemarrage de service, redemarrage du poste ou scan Windows Update force n'est declenche.
|
||||
|
||||
## Sources
|
||||
|
||||
- Microsoft (strategie cible) : https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-update
|
||||
- Microsoft (etats/version de Windows 11) : https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information
|
||||
- Tranquil IT WAPT 2.6 (packages) : https://www.wapt.fr/fr/doc-2.6/wapt-package-structure.html
|
||||
- Tranquil IT WAPT 2.6 (build/sign) : https://www.wapt.fr/fr/doc-2.6/wapt-command-line-interface.html
|
||||
@@ -0,0 +1,15 @@
|
||||
package : comi-win11-max-25h2
|
||||
version : 1.0.0-0
|
||||
architecture : all
|
||||
section : base
|
||||
priority : optional
|
||||
maintainer : Comitari
|
||||
description : Pin Windows 11 feature updates to 25H2 maximum; keep quality updates enabled
|
||||
description_fr : Limite les mises a jour de fonctionnalites Windows 11 a 25H2 sans bloquer les correctifs de securite
|
||||
maturity : PROD
|
||||
target_os : windows
|
||||
min_os_version : 10.0.22000
|
||||
min_wapt_version : 2.0
|
||||
audit_schedule : 1d
|
||||
keywords : windows11,windows-update,25h2,policy
|
||||
homepage : https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-update
|
||||
@@ -0,0 +1,223 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""WAPT: keep Windows 11 at or below feature update 25H2.
|
||||
|
||||
Uses the Microsoft target feature update policy. No Windows Update service,
|
||||
quality update settings, deferrals, or WSUS settings are modified.
|
||||
"""
|
||||
|
||||
from setuphelpers import *
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import winreg
|
||||
|
||||
uninstallkey = []
|
||||
|
||||
PACKAGE_NAME = 'comi-win11-max-25h2'
|
||||
POLICY_PATH = r'SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'
|
||||
CURRENT_VERSION_PATH = r'SOFTWARE\Microsoft\Windows NT\CurrentVersion'
|
||||
TARGET_VERSION = '25H2'
|
||||
EXPECTED = {
|
||||
'ProductVersion': ('Windows 11', winreg.REG_SZ),
|
||||
'TargetReleaseVersion': (1, winreg.REG_DWORD),
|
||||
'TargetReleaseVersionInfo': (TARGET_VERSION, winreg.REG_SZ),
|
||||
}
|
||||
STATE_DIR = os.path.join(
|
||||
os.environ.get('ProgramData', r'C:\ProgramData'),
|
||||
'Comitari', 'WAPT', PACKAGE_NAME,
|
||||
)
|
||||
BACKUP_FILE = os.path.join(STATE_DIR, 'previous_policy.json')
|
||||
REGISTRY_VIEW = winreg.KEY_WOW64_64KEY
|
||||
|
||||
|
||||
def _read_value(path, name):
|
||||
"""Return (value, reg_type), or None when path/value is absent."""
|
||||
try:
|
||||
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path, 0,
|
||||
winreg.KEY_QUERY_VALUE | REGISTRY_VIEW) as key:
|
||||
return winreg.QueryValueEx(key, name)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def _read_os():
|
||||
build_info = _read_value(CURRENT_VERSION_PATH, 'CurrentBuildNumber')
|
||||
install_info = _read_value(CURRENT_VERSION_PATH, 'InstallationType')
|
||||
edition_info = _read_value(CURRENT_VERSION_PATH, 'EditionID')
|
||||
display_info = _read_value(CURRENT_VERSION_PATH, 'DisplayVersion')
|
||||
if not build_info:
|
||||
raise RuntimeError('Impossible de lire la build de Windows')
|
||||
try:
|
||||
build = int(build_info[0])
|
||||
except (ValueError, TypeError):
|
||||
raise RuntimeError('Build Windows illisible : %r' % (build_info[0],))
|
||||
installation = str(install_info[0]) if install_info else ''
|
||||
edition = str(edition_info[0]) if edition_info else ''
|
||||
display = str(display_info[0]) if display_info else ''
|
||||
return build, installation, edition, display
|
||||
|
||||
|
||||
def _assert_supported_os():
|
||||
build, installation, edition, display = _read_os()
|
||||
# Windows Server 2025 has build 26100 too: build alone is insufficient.
|
||||
if build < 22000 or installation.lower() != 'client':
|
||||
raise RuntimeError(
|
||||
'Ce paquet est reserve aux postes Windows 11 (build >= 22000); '
|
||||
'build=%s, installation=%s' % (build, installation)
|
||||
)
|
||||
# LTSC editions have a different servicing model and do not target 25H2.
|
||||
if edition.lower().startswith(('enterprises', 'iotenterprises')):
|
||||
raise RuntimeError('Edition LTSC non prise en charge : %s' % edition)
|
||||
if edition.lower().startswith('core'):
|
||||
raise RuntimeError('Edition Home non prise en charge officiellement : %s' % edition)
|
||||
return build, display
|
||||
|
||||
|
||||
def _version_tuple(value):
|
||||
match = re.fullmatch(r'(\d{2})H([12])', value.strip(), re.IGNORECASE)
|
||||
if not match:
|
||||
return None
|
||||
return int(match.group(1)), int(match.group(2))
|
||||
|
||||
|
||||
def _snapshot_policy():
|
||||
"""Save the pre-existing values once, also across WAPT reinstalls."""
|
||||
if os.path.isfile(BACKUP_FILE):
|
||||
# Fail closed if a backup exists but is unreadable.
|
||||
with open(BACKUP_FILE, 'r', encoding='utf-8') as stream:
|
||||
saved = json.load(stream)
|
||||
if saved.get('schema_version') != 1 or saved.get('package') != PACKAGE_NAME:
|
||||
raise RuntimeError('Sauvegarde de strategie incompatible : ' + BACKUP_FILE)
|
||||
if set(saved.get('values', {})) != set(EXPECTED):
|
||||
raise RuntimeError('Sauvegarde de strategie incomplete : ' + BACKUP_FILE)
|
||||
return
|
||||
|
||||
state = {'schema_version': 1, 'package': PACKAGE_NAME, 'values': {}}
|
||||
for name in EXPECTED:
|
||||
previous = _read_value(POLICY_PATH, name)
|
||||
state['values'][name] = (
|
||||
{'present': False}
|
||||
if previous is None else
|
||||
{'present': True, 'value': previous[0], 'type': previous[1]}
|
||||
)
|
||||
|
||||
os.makedirs(STATE_DIR, exist_ok=True)
|
||||
descriptor, tmp_path = tempfile.mkstemp(prefix='.previous-policy-',
|
||||
suffix='.tmp', dir=STATE_DIR)
|
||||
try:
|
||||
with os.fdopen(descriptor, 'w', encoding='utf-8') as stream:
|
||||
json.dump(state, stream, ensure_ascii=False, indent=2)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(tmp_path, BACKUP_FILE)
|
||||
finally:
|
||||
if os.path.exists(tmp_path):
|
||||
os.unlink(tmp_path)
|
||||
|
||||
|
||||
def _write_policy():
|
||||
with winreg.CreateKeyEx(winreg.HKEY_LOCAL_MACHINE, POLICY_PATH, 0,
|
||||
winreg.KEY_SET_VALUE | REGISTRY_VIEW) as key:
|
||||
for name, (value, kind) in EXPECTED.items():
|
||||
current = _read_value(POLICY_PATH, name)
|
||||
if current != (value, kind):
|
||||
winreg.SetValueEx(key, name, 0, kind, value)
|
||||
print('Policy %s = %r' % (name, value))
|
||||
else:
|
||||
print('Policy %s deja conforme' % name)
|
||||
|
||||
|
||||
def install():
|
||||
build, display = _assert_supported_os()
|
||||
current_version = _version_tuple(display)
|
||||
# 25H2 uses build family 26200. Newer release families cannot downgrade.
|
||||
if build >= 26300:
|
||||
raise RuntimeError(
|
||||
'Ce poste utilise deja une build posterieure a 25H2 '
|
||||
'(build %s, DisplayVersion=%s); downgrade impossible.' % (build, display)
|
||||
)
|
||||
if current_version is not None and current_version > _version_tuple(TARGET_VERSION):
|
||||
raise RuntimeError(
|
||||
'Ce poste est deja en Windows 11 %s (build %s). '
|
||||
'Un verrouillage 25H2 ne peut pas effectuer de downgrade.'
|
||||
% (display, build)
|
||||
)
|
||||
if current_version is None:
|
||||
print('WARNING: DisplayVersion non reconnu (%r), verifier la version OS.' % display)
|
||||
_snapshot_policy()
|
||||
_write_policy()
|
||||
print('Windows Update cible Windows 11 %s; mises a jour qualite inchangees.'
|
||||
% TARGET_VERSION)
|
||||
|
||||
|
||||
def audit():
|
||||
try:
|
||||
build, display = _assert_supported_os()
|
||||
except Exception as exc:
|
||||
print('ERROR: OS incompatible : %s' % exc)
|
||||
return 'ERROR'
|
||||
|
||||
failures = []
|
||||
for name, expected in EXPECTED.items():
|
||||
actual = _read_value(POLICY_PATH, name)
|
||||
if actual != expected:
|
||||
failures.append('%s attendu=%r obtenu=%r' % (name, expected, actual))
|
||||
if failures:
|
||||
print('ERROR: strategie absente, alteree ou surchargee : %s'
|
||||
% '; '.join(failures))
|
||||
return 'ERROR'
|
||||
|
||||
current_version = _version_tuple(display)
|
||||
if current_version is None:
|
||||
print('WARNING: policy conforme, mais version Windows inconnue : '
|
||||
'DisplayVersion=%r, build=%s' % (display, build))
|
||||
return 'WARNING'
|
||||
if current_version > _version_tuple(TARGET_VERSION):
|
||||
print('ERROR: Windows 11 %s deja superieur a %s : downgrade impossible'
|
||||
% (display, TARGET_VERSION))
|
||||
return 'ERROR'
|
||||
print('OK: Windows 11 %s (build %s), policy cible %s'
|
||||
% (display, build, TARGET_VERSION))
|
||||
return 'OK'
|
||||
|
||||
|
||||
def uninstall():
|
||||
"""Restore initial settings without clobbering externally changed values.
|
||||
|
||||
On some WAPT versions this must be triggered explicitly with
|
||||
`wapt-get uninstall <package>` before `wapt-get remove <package>`.
|
||||
"""
|
||||
if not os.path.isfile(BACKUP_FILE):
|
||||
print('WARNING: aucune sauvegarde; pas de suppression aveugle des policies')
|
||||
return
|
||||
with open(BACKUP_FILE, 'r', encoding='utf-8') as stream:
|
||||
state = json.load(stream)
|
||||
if state.get('schema_version') != 1 or state.get('package') != PACKAGE_NAME:
|
||||
raise RuntimeError('Sauvegarde de strategie incompatible : ' + BACKUP_FILE)
|
||||
if set(state.get('values', {})) != set(EXPECTED):
|
||||
raise RuntimeError('Sauvegarde de strategie incomplete : ' + BACKUP_FILE)
|
||||
|
||||
with winreg.CreateKeyEx(winreg.HKEY_LOCAL_MACHINE, POLICY_PATH, 0,
|
||||
winreg.KEY_SET_VALUE | REGISTRY_VIEW) as key:
|
||||
for name, (ours, our_kind) in EXPECTED.items():
|
||||
current = _read_value(POLICY_PATH, name)
|
||||
if current != (ours, our_kind):
|
||||
print('WARNING: %s modifie par un autre mecanisme; conserve : %r'
|
||||
% (name, current))
|
||||
continue
|
||||
before = state['values'][name]
|
||||
if before['present']:
|
||||
winreg.SetValueEx(key, name, 0, before['type'], before['value'])
|
||||
print('Restauration : %s' % name)
|
||||
else:
|
||||
winreg.DeleteValue(key, name)
|
||||
print('Suppression : %s' % name)
|
||||
# Keep the WindowsUpdate parent key: it may contain other managed values.
|
||||
os.unlink(BACKUP_FILE)
|
||||
try:
|
||||
os.rmdir(STATE_DIR)
|
||||
except OSError:
|
||||
pass
|
||||
print('Restauration terminee (les parametres externes sont preserves).')
|
||||
Reference in new issue
Block a user