Es gibt eine Kategorie von Fehlern, die Ihnen kein Fehlerprotokoll je zeigen wird, weil nichts fehlschlägt. Die API antwortet. Der Wert lässt sich parsen. Der Bericht wird gezeichnet. Und die Antwort ist auf genau die Art falsch, die Karrieren beendet: Sie ist falsch mit dem Auftreten von Autorität.
Wir sind ihr in einer einzigen Saison dreimal begegnet, beim Bau einer Evidenz-Engine für die Governance von Microsoft 365. Jedes Mal war der Mechanismus anders, jedes Mal ist er reproduzierbar, und jedes Mal war die Korrektur strukturell und kein Flicken. Dies ist die lange Fassung, mit dem Code.
Gestalt eins: der Standardwert, der sich wie ein Befund liest
SharingCapability ist die Eigenschaft, die angibt, welches externe Teilen eine SharePoint-Site höchstens erlaubt. Ihre vier Werte, nach Grad der Offenlegung:
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 identityDer naheliegende Weg, einen Tenant zu inventarisieren, ist, jede Site einmal zu enumerieren und die Eigenschaft von jeder abzulesen:
# The tempting one-liner. It is also wrong, and Microsoft says so.
Get-SPOSite -Limit All | Select-Object Url, SharingCapabilityHier ist der Hinweis, den Microsoft auf die Get-SPOSite-Referenz setzt, wörtlich zitiert:
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.
Dreiundzwanzig Eigenschaften. Sobald Sie -Limit oder -Filter übergeben, wird keine gelesen; jede liefert den Standardwert ihres Typs. SharingCapability ist ein .NET-Enum, sein Standardwert ist das Null-Mitglied, und das Null-Mitglied ist Disabled.
Lesen Sie die Kette noch einmal langsam. Die Massen-Enumeration, das Erste, wonach jedes Inventarskript greift, liefert den verriegeltsten Wert des Vokabulars für jede Eigenschaft, die sie nicht gelesen hat. Nicht null. Kein Fehler. Das sicherste mögliche Wort, für eine Site, die sie nie angesehen hat.
Die korrekte Lesung ist pro Site, über die Identität:
# 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).SharingCapabilityDie beiden widersprechen sich, und zwar still. Wir haben es gegen einen echten Tenant reproduziert, und eine von fünf Sites wich ab: die Enumeration sagte Disabled, die Direktlesung sagte ExternalUserAndGuestSharing. Eine Site, die Anyone-Links verteilt, gemeldet als versiegelt, von einer korrekten API, die genau das tut, was ihre Dokumentation sagt.
Ein Dashboard auf Basis des ersten Ausschnitts zeigt eine Wand aus Grün. Jede Kachel ist ein funktionierender API-Aufruf. Jede Kachel kann falsch sein.
Die Ingenieursantwort lautet nicht „denk immer daran, pro Site zu lesen". Menschen erinnern sich nicht zuverlässig. Die Antwort lautet: Ein Wert, gelesen über einen Pfad, von dem bekannt ist, dass er ihn nicht befüllt, muss strukturell als keine Evidenz verzeichnet werden, damit nichts stromabwärts ihn konsumieren kann:
# 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'
}Die Enumeration wird auf das herabgestuft, wofür sie ehrlich taugt (eine Liste von Adressen zu erzeugen), und die Tatsache wird stets aus der Lesung genommen, die Microsoft als vollständig dokumentiert.
Gestalt zwei: die Herkunft, die niemand prüfte
Die zweite fanden wir in unserem eigenen Repository, dem ehrlichen Ort dafür.
Jedes Evidenzdokument, das wir ausliefern, trägt einen provenance-Block: wann es gesammelt wurde, womit, über welche API. Siebenundzwanzig Fixtures erklärten dies:
{
"provenance": {
"collected_at": "2026-08-05T14:02:11Z",
"collector": "spo-collector",
"source_api": "Microsoft Graph v1.0"
}
}Der Collector hat Graph nie benutzt. Er liest über PnP.PowerShell und CSOM; seine Tenant-Pfade laufen über die SharePoint-Admin-API. Microsoft Graph v1.0 war aus einer Annahme der ersten Woche fortkopiert und nie hinterfragt worden, denn jede Prüfung, die wir hatten, sah so aus:
# What the schema enforced: source_api is a string. It was a beautiful string.
assert isinstance(doc["provenance"]["source_api"], str)Das Feld, für das das ganze Produkt existiert, um es zu garantieren, woher wissen Sie das?, war in siebenundzwanzig Dokumenten falsch, und jedes Tor stand auf Grün, weil das Tor die Form der Behauptung prüfte und nie ihre Wahrheit.
Die Korrektur bindet die Behauptung an die Wirklichkeit. Es gibt jetzt ein Tor, das liest, welche Sammelpfade der Collector wirklich deklariert, und jede source_api ablehnt, die keiner davon ist:
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}"Der erste Entwurf des Tors selbst war falsch: Er las nur die Module und verpasste den Orchestrator, beschuldigte also korrekte Admin-Evidenz der Lüge. Dieser Fehler ist absichtlich in der Historie bewahrt. Ein Prüfer, der einmal falsch lag, kann wieder falsch liegen, und sich zu erinnern, wie genau, ist die billigste Versicherung, die es gibt.
Gestalt drei: der Zeitstempel, der eine Konstruktion als Lesung ausgab
Die subtilste. Unser öffentliches Beispielergebnis zeigte eine echte Regel, die gegen eine Dokumentbibliothek fehlschlug, gestempelt Collected: 2026-08-05T14:02:11Z. Die Regellogik stimmte. Die Zahl stimmte. Die Bibliothek existierte nie: die Evidenz war eine Fixture, von Hand gebaut, um einen Codepfad zu üben, mit einem Sammelzeitstempel, weil der Renderer für jedes Ergebnis einen druckte.
Nichts auf dieser Seite war Satz für Satz falsch. Zusammengesetzt behauptete sie etwas Falsches: dass dies in einem Tenant beobachtet worden sei. Der Abstand zwischen einem wahren Ergebnis über eine Konstruktion und der Beobachtung eines Tenants ist der ganze Abstand zwischen einer Demo und einem Befund, und die Seite hatte ihn gelöscht.
Die Reparatur ist ein Register, das außerhalb der Evidenzdokumente lebt und jede Fixture nach Herkunft klassifiziert:
{
"path": "fixtures/sharepoint/list-over-limit.json",
"origin": "synthetic",
"may_be_presented_as_tenant_observation": false
}Und ein Test, der verweigert, dass das Evidenzschema je das Wort lernt, das die Unterscheidung einstürzen ließe:
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))Zwei Fragen, zwei Wohnorte: was die Evidenz ist, lebt im Schema; wofür eine Datei da ist, lebt im Register. An dem Tag, an dem ein konstruiertes Dokument als gesammelte Evidenz validieren kann, ist der Unterschied für alle verschwunden.
Warum die selbstsichere falsche Antwort die schlimmere ist
Drei Gestalten, eine Wurzel. In jedem Fall zog ein System es vor, eine Antwort zu produzieren, statt die Grenzen seines Wissens einzuräumen. Das Enum hatte einen Standard, also antwortete es. Die Zeichenkette validierte, also bestand sie. Der Zeitstempel existierte, also las er sich als beobachtet.
Die Disziplin, die alle drei fängt, ist unbequem, weil sie Berichte schlechter aussehen lässt, bevor sie sie ehrlich macht: unknown ist ein gültiges Ergebnis, und es ist das ehrliche, wann immer die Evidenz nicht wirklich gelesen wurde. Die Engine hat sechs Ausgänge, und zwei davon beschreiben die Maschinerie und nicht den Tenant:
pass fail not-applicable unknown invalid-evidence errorunknown heißt, die Evidenz war nicht da, um gelesen zu werden: sammeln Sie erneut, mit dem Zugriff, der Ihnen fehlte. Es wird durch eine feste Reihenfolge aufgelöst, die keine Engine umordnen darf: ein erforderlicher Pfad im Zustand missing, not-supported oder permission-denied erzwingt unknown, bevor irgendein pass oder fail erreicht werden kann. Eine Site, deren SharingCapability aus einer Enumeration kam, ist weder konform noch verletzend. Sie ist ungelesen, und der Bericht muss das sagen.
Wägen Sie nun die beiden Fehler ab. Fehlende Evidenz sagt Ihnen, wo Sie suchen müssen: „16 Sites unter dieser Identität unlesbar" ist eine benannte Liste und ein nächster Schritt. Die selbstsichere falsche Antwort sagt Ihnen, aufzuhören zu suchen. Sie gibt Ihr Aufmerksamkeitsbudget für die Sites aus, die es am wenigsten brauchen, während die Site, die Disabled meldet, im Stillen anonyme Links an jeden verteilt, der fragt.
Wenn Sie eine Gewohnheit aus diesem Text mitnehmen, dann diese: Verfolgen Sie für jeden Wert auf Ihren Dashboards den Lesepfad und fragen Sie, was er zurückgibt, wenn er das Feld nicht befüllen kann. Ist die Antwort etwas anderes als eine sichtbare Verweigerung, haben Sie keine Überwachungslücke. Sie haben eine Vertrauensmaschine, und sie ist auf die Dinge gerichtet, die Sie am wenigsten beunruhigen.