Há uma categoria de falha que nenhum registo de erros lhe vai mostrar, porque nada falha. A API responde. O valor faz parse. O relatório desenha-se. E a resposta está errada da maneira específica que acaba carreiras: está errada com ar de autoridade.
Batemos nela três vezes numa só época, a construir um motor de evidência para a governação do Microsoft 365. De cada vez o mecanismo foi diferente, de cada vez é reproduzível, e de cada vez a correção foi estrutural e não um remendo. Esta é a versão longa, com o código.
Forma um: a omissão que se lê como conclusão
O SharingCapability é a propriedade que diz qual é a partilha externa mais permissiva que um site do SharePoint autoriza. Os quatro valores, por ordem de exposição:
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 identityA maneira óbvia de inventariar um tenant é enumerar cada site uma vez e ler a propriedade de cada um:
# The tempting one-liner. It is also wrong, and Microsoft says so.
Get-SPOSite -Limit All | Select-Object Url, SharingCapabilityAqui está a nota que a Microsoft põe na referência do Get-SPOSite, citada tal e qual:
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.
Vinte e três propriedades. No momento em que passa -Limit ou -Filter, nenhuma é lida; cada uma devolve o valor por omissão do seu tipo. O SharingCapability é um enum de .NET, o seu valor por omissão é o membro zero, e o membro zero é Disabled.
Leia a cadeia outra vez, devagar. A enumeração em bloco, a primeira coisa a que qualquer script de inventário estende a mão, devolve o valor mais fechado do vocabulário para cada propriedade que não leu. Não é null. Não é um erro. É a palavra mais segura possível, num site para o qual nunca chegou a olhar.
A leitura correta é por site, pela identidade:
# 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).SharingCapabilityAs duas discordam, e discordam em silêncio. Reproduzimo-lo contra um tenant real e um site em cinco divergia: a enumeração dizia Disabled, a leitura direta dizia ExternalUserAndGuestSharing. Um site a distribuir ligações Anyone, reportado como selado, por uma API correta a fazer exatamente o que a documentação dela diz.
Um painel construído sobre o primeiro excerto mostra uma parede de verde. Cada azulejo é uma chamada de API que funciona. Cada azulejo pode estar errado.
A resposta de engenharia não é "lembrar-se sempre de ler por site". Ninguém se lembra de forma fiável. A resposta é que um valor lido por um caminho que se sabe não populá-lo tem de ser registado como não sendo evidência, de forma estrutural, para que nada a jusante o possa consumir:
# 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'
}A enumeração é rebaixada àquilo para que serve honestamente (produzir uma lista de endereços) e o facto é sempre tirado da leitura que a Microsoft documenta como completa.
Forma dois: a proveniência que ninguém verificou
A segunda encontrámo-la no nosso próprio repositório, que é o sítio honesto para a encontrar.
Cada documento de evidência que entregamos traz um bloco provenance: quando foi recolhido, por quê, através de que API. Vinte e sete fixtures declaravam isto:
{
"provenance": {
"collected_at": "2026-08-05T14:02:11Z",
"collector": "spo-collector",
"source_api": "Microsoft Graph v1.0"
}
}O colector nunca usou o Graph. Lê através do PnP.PowerShell e de CSOM; os caminhos de tenant passam pela API de administração do SharePoint. O Microsoft Graph v1.0 foi copiado para a frente a partir de uma suposição da primeira semana e nunca foi questionado, porque toda a verificação que tínhamos era assim:
# What the schema enforced: source_api is a string. It was a beautiful string.
assert isinstance(doc["provenance"]["source_api"], str)O campo que o produto inteiro existe para garantir, como é que sabe isto?, estava falso em vinte e sete documentos, e todos os portões estavam verdes, porque o portão verificava a forma da afirmação e nunca a sua verdade.
A correção amarra a afirmação à realidade. Existe agora um portão que lê que caminhos de recolha o colector realmente declara, e recusa qualquer source_api que não seja um deles:
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}"O primeiro rascunho do próprio portão estava errado: lia só os módulos e falhava o orquestrador, e por isso acusava evidência correta da administração de mentir. Esse erro está preservado no histórico de propósito. Um verificador que já esteve errado pode voltar a estar, e lembrar exatamente como é o seguro mais barato que há.
Forma três: o registo de hora que fez uma construção passar por leitura
A mais subtil. O nosso resultado público de exemplo mostrava uma regra real a falhar contra uma biblioteca de documentos, carimbada Collected: 2026-08-05T14:02:11Z. A lógica da regra estava certa. O número estava certo. A biblioteca nunca existiu: a evidência era uma fixture, feita à mão para exercitar um caminho de código, com um carimbo de recolha porque o renderizador imprimia um para todos os resultados.
Nada naquela página era falso frase a frase. Montada, afirmava uma coisa falsa: que aquilo tinha sido observado num tenant. A distância entre um resultado verdadeiro sobre uma construção e a observação de um tenant é toda a distância entre uma demonstração e uma conclusão, e a página tinha-a apagado.
A reparação é um registo que vive fora dos documentos de evidência e classifica cada fixture pela origem:
{
"path": "fixtures/sharepoint/list-over-limit.json",
"origin": "synthetic",
"may_be_presented_as_tenant_observation": false
}E um teste que recusa que o esquema de evidência alguma vez aprenda a palavra que colapsaria a distinção:
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))Duas perguntas, duas casas: o que a evidência é vive no esquema; para que serve um ficheiro vive no registo. No dia em que um documento construído puder validar como evidência recolhida, a diferença desaparece para qualquer leitor.
Porque a resposta errada e confiante é a pior
Três formas, uma raiz. Em cada caso, um sistema preferiu produzir uma resposta a admitir os limites do que sabia. O enum tinha uma omissão, portanto respondeu. A string validava, portanto passou. O carimbo existia, portanto leu-se como observado.
A disciplina que apanha as três é desconfortável, porque faz os relatórios parecerem piores antes de os fazer honestos: unknown é um resultado válido, e é o honesto sempre que a evidência não foi realmente lida. O motor tem seis desfechos, e dois deles descrevem a maquinaria e não o tenant:
pass fail not-applicable unknown invalid-evidence errorunknown quer dizer que a evidência não estava lá para ler: recolha outra vez, com o acesso que lhe faltava. Resolve-se por uma ordem fixa que motor nenhum é livre de reordenar: um caminho obrigatório em estado missing, not-supported ou permission-denied força unknown antes de se poder chegar a qualquer pass ou fail. Um site cujo SharingCapability veio de uma enumeração não está em conformidade nem em violação. Está por ler, e o relatório tem de o dizer.
Agora pese as duas falhas. A evidência em falta diz-lhe onde procurar: "16 sites ilegíveis com esta identidade" é uma lista com nome e um passo seguinte. A resposta errada e confiante diz-lhe para parar de procurar. Gasta o seu orçamento de atenção nos sites que menos precisam dele, enquanto o site que reporta Disabled distribui ligações anónimas a quem as pedir, em silêncio.
Se guardar um hábito deste texto, que seja este: para cada valor nos seus painéis, siga o caminho de leitura e pergunte o que ele devolve quando não consegue popular o campo. Se a resposta for outra coisa que não uma recusa visível, não tem uma lacuna de monitorização. Tem uma máquina de confiança, e está apontada às coisas com que menos se preocupa.