first upload

This commit is contained in:
PierreCsn committed 2026-10-27 17:15:21 +01:00
commit ed1485646a
3 files changed
+341

No files matched your search

+103
View File
@@ -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
+15
View File
@@ -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
+223
View File
@@ -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).')