Il existe une catégorie de défaillance qu'aucun journal d'erreurs ne vous montrera jamais, parce que rien n'échoue. L'API répond. La valeur se parse. Le rapport s'affiche. Et la réponse est fausse de la manière précise qui met fin aux carrières : elle est fausse avec un air d'autorité.
Nous l'avons rencontrée trois fois en une seule saison, en construisant un moteur de preuve pour la gouvernance de Microsoft 365. Chaque fois le mécanisme était différent, chaque fois il est reproductible, et chaque fois le correctif fut structurel et non un rustine. Voici la version longue, avec le code.
Forme un : le défaut qui se lit comme un constat
SharingCapability est la propriété qui indique le partage externe le plus permissif qu'un site SharePoint autorise. Ses quatre valeurs, par ordre d'exposition :
Disabled no external sharing
ExistingExternalUserSharingOnly guests already in the directory
ExternalUserSharingOnly new and existing guests, sign-in required
ExternalUserAndGuestSharing Anyone links: no sign-in, no identityLa façon évidente d'inventorier un tenant est d'énumérer chaque site une fois et de lire la propriété sur chacun :
# The tempting one-liner. It is also wrong, and Microsoft says so.
Get-SPOSite -Limit All | Select-Object Url, SharingCapabilityVoici la note que Microsoft met sur la référence de Get-SPOSite, citée telle quelle :
If the Limit or Filter parameters are provided then the following site collection properties will not be populated and may contain a default value: AllowDownloadingNonWebViewableFiles, AllowEditing, [...] DefaultLinkPermission, DefaultSharingLinkType, [...] SensitivityLabel, [...] SharingCapability, SharingDomainRestrictionMode.
Vingt-trois propriétés. Dès que vous passez -Limit ou -Filter, aucune n'est lue ; chacune renvoie la valeur par défaut de son type. SharingCapability est un enum .NET, sa valeur par défaut est le membre zéro, et le membre zéro est Disabled.
Relisez la chaîne lentement. L'énumération en masse, la première chose vers laquelle tend tout script d'inventaire, renvoie la valeur la plus verrouillée du vocabulaire pour toute propriété qu'elle n'a pas lue. Pas null. Pas une erreur. Le mot le plus sûr possible, sur un site qu'elle n'a jamais regardé.
La lecture correcte est par site, par identité :
# One site, actually populated. Note the admin endpoint: sharing capability
# is a tenant property about a site, not a property of the site object.
Connect-PnPOnline -Url https://contoso-admin.sharepoint.com -Interactive -ClientId $appId
(Get-PnPTenantSite -Identity https://contoso.sharepoint.com/sites/finance).SharingCapabilityLes deux divergent, et elles divergent en silence. Nous l'avons reproduit contre un tenant réel et un site sur cinq différait : l'énumération disait Disabled, la lecture directe disait ExternalUserAndGuestSharing. Un site distribuant des liens Anyone, rapporté comme scellé, par une API correcte faisant exactement ce que dit sa documentation.
Un tableau de bord bâti sur le premier extrait montre un mur de vert. Chaque tuile est un appel d'API qui fonctionne. Chaque tuile peut être fausse.
La réponse d'ingénierie n'est pas « pense toujours à lire par site ». Personne ne s'en souvient de façon fiable. La réponse est qu'une valeur lue par un chemin dont on sait qu'il ne la renseigne pas doit être enregistrée comme n'étant pas une preuve, structurellement, pour que rien en aval ne puisse la consommer :
# From the collector. The enumerated value is never trusted; it is only
# used to obtain the list of URLs to read properly.
$urls = Get-PnPTenantSite | Select-Object -ExpandProperty Url # the list, not the facts
foreach ($u in $urls) {
$site = Get-PnPTenantSite -Identity $u # the populated read
New-ScalarFact -Value ([string]$site.SharingCapability) -RawField 'SharingCapability'
}L'énumération est rétrogradée à ce à quoi elle sert honnêtement (produire une liste d'adresses) et le fait n'est jamais pris que de la lecture que Microsoft documente comme complète.
Forme deux : la provenance que personne n'a vérifiée
La deuxième, nous l'avons trouvée dans notre propre dépôt, l'endroit honnête pour la trouver.
Chaque document de preuve que nous livrons porte un bloc provenance : quand il a été collecté, par quoi, via quelle API. Vingt-sept fixtures déclaraient ceci :
{
"provenance": {
"collected_at": "2026-08-05T14:02:11Z",
"collector": "spo-collector",
"source_api": "Microsoft Graph v1.0"
}
}Le collecteur n'a jamais utilisé Graph. Il lit via PnP.PowerShell et CSOM ; ses chemins de tenant passent par l'API d'administration de SharePoint. Microsoft Graph v1.0 avait été recopié depuis une supposition de la première semaine et jamais remis en question, parce que toute vérification que nous avions ressemblait à ceci :
# What the schema enforced: source_api is a string. It was a beautiful string.
assert isinstance(doc["provenance"]["source_api"], str)Le champ que le produit entier existe pour garantir, comment le savez-vous ?, était faux dans vingt-sept documents, et toutes les portes étaient au vert, parce que la porte vérifiait la forme de l'affirmation et jamais sa vérité.
Le correctif attache l'affirmation à la réalité. Il existe désormais une porte qui lit quels chemins de collecte le collecteur déclare réellement, et refuse tout source_api qui n'en fait pas partie :
def test_no_fixture_claims_an_api_the_collector_never_uses():
# The paths the collector really declares, read from its own source.
paths = set()
for f in [ORCHESTRATOR, *MODULES.glob("*.psm1")]:
paths |= set(re.findall(r"-SourceApi\s+'([^']+)'", f.read_text()))
# Every published source_api must be one the collector can produce.
bad = [p.name for p in FIXTURES.rglob("*.json")
if (api := load(p)["provenance"].get("source_api")) and api not in paths]
assert not bad, f"fixtures claim an API the collector never uses: {bad}"Le premier brouillon de la porte elle-même était faux : il ne lisait que les modules et manquait l'orchestrateur, accusant donc de mensonge une preuve d'administration correcte. Cette erreur est préservée dans l'historique exprès. Un vérificateur qui s'est trompé une fois peut se tromper encore, et se souvenir exactement comment est l'assurance la moins chère qui soit.
Forme trois : l'horodatage qui a fait passer une construction pour une lecture
La plus subtile. Notre résultat public d'exemple montrait une vraie règle échouant contre une bibliothèque de documents, estampillée Collected: 2026-08-05T14:02:11Z. La logique de la règle était juste. Le nombre était juste. La bibliothèque n'a jamais existé : la preuve était une fixture, faite à la main pour exercer un chemin de code, portant un horodatage de collecte parce que le moteur de rendu en imprimait un pour chaque résultat.
Rien sur cette page n'était faux phrase par phrase. Assemblée, elle affirmait quelque chose de faux : que cela avait été observé dans un tenant. La distance entre un résultat vrai sur une construction et l'observation d'un tenant est toute la distance entre une démonstration et un constat, et la page l'avait effacée.
La réparation est un registre qui vit hors des documents de preuve et classe chaque fixture par son origine :
{
"path": "fixtures/sharepoint/list-over-limit.json",
"origin": "synthetic",
"may_be_presented_as_tenant_observation": false
}Et un test qui refuse que le schéma de preuve apprenne jamais le mot qui effondrerait la distinction :
def test_the_evidence_schema_knows_nothing_about_fixtures():
# `acquisition` says how REAL evidence arrived: collected | imported.
# Teaching it `synthetic` would let a production Assessment validate a
# construction. The classification lives in the registry, not the schema.
assert "synthetic" not in json.dumps(load(EVIDENCE_SCHEMA))Deux questions, deux maisons : ce que la preuve est vit dans le schéma ; à quoi sert un fichier vit dans le registre. Le jour où un document construit peut valider comme preuve collectée, la différence disparaît pour tout le monde.
Pourquoi la réponse fausse et sûre d'elle est la pire
Trois formes, une racine. Dans chaque cas, un système a préféré produire une réponse plutôt qu'admettre les limites de ce qu'il savait. L'enum avait un défaut, donc il a répondu. La chaîne validait, donc elle est passée. L'horodatage existait, donc il s'est lu comme observé.
La discipline qui attrape les trois est inconfortable, parce qu'elle rend les rapports moins flatteurs avant de les rendre honnêtes : unknown est un résultat valide, et c'est l'honnête chaque fois que la preuve n'a pas réellement été lue. Le moteur a six issues, et deux d'entre elles décrivent la machinerie et non le tenant :
pass fail not-applicable unknown invalid-evidence errorunknown veut dire que la preuve n'était pas là pour être lue : collectez à nouveau, avec l'accès qui vous manquait. Cela se résout par un ordre fixe qu'aucun moteur n'est libre de réordonner : un chemin requis en état missing, not-supported ou permission-denied force unknown avant que tout pass ou fail puisse être atteint. Un site dont le SharingCapability vient d'une énumération n'est ni conforme ni en violation. Il est non lu, et le rapport doit le dire.
Pesez maintenant les deux défaillances. La preuve manquante vous dit où chercher : « 16 sites illisibles sous cette identité » est une liste nommée et une étape suivante. La réponse fausse et sûre d'elle vous dit d'arrêter de chercher. Elle dépense votre budget d'attention sur les sites qui en ont le moins besoin, pendant que le site qui rapporte Disabled distribue des liens anonymes à qui les demande, en silence.
Si vous ne gardez qu'une habitude de ce texte, que ce soit celle-ci : pour chaque valeur de vos tableaux de bord, suivez le chemin de lecture et demandez ce qu'il renvoie quand il ne peut pas renseigner le champ. Si la réponse est autre chose qu'un refus visible, vous n'avez pas une lacune de supervision. Vous avez une machine à confiance, et elle est pointée sur les choses qui vous inquiètent le moins.