TenantPulse.psd1

#
# Module manifest for module 'TenantPulse'
#
# Generated by: Adam Gell
#
# Generated on: 8/15/2026
#

@{

# Script module or binary module file associated with this manifest.
RootModule = 'TenantPulse.psm1'

# Version number of this module.
ModuleVersion = '0.1.0'

# Supported PSEditions
# CompatiblePSEditions = @()

# ID used to uniquely identify this module
GUID = 'a2f6d0f0-6d0c-4a6b-9f7e-9c9e6f6c7c2f'

# Author of this module
Author = 'Adam Gell'

# Company or vendor of this module
CompanyName = 'Adam Gell'

# Copyright statement for this module
Copyright = '(c) 2026 Adam Gell. Licensed under MIT.'

# Description of the functionality provided by this module
Description = 'Read-only tenant health engine that evaluates Microsoft Intune and Entra configuration against a versioned set of checks, producing a deterministic, pseudonymized, scored snapshot.'

# Minimum version of the PowerShell engine required by this module
PowerShellVersion = '7.4'

# Name of the PowerShell host required by this module
# PowerShellHostName = ''

# Minimum version of the PowerShell host required by this module
# PowerShellHostVersion = ''

# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# DotNetFrameworkVersion = ''

# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# ClrVersion = ''

# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''

# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @(
    # TenantPulse reads the tenant exclusively through GraphKit's read-class descriptors.
    # See the design spec: no Graph SDK, no Connect-MgGraph, no URI strings of its own.
    # GraphKit 0.1.1 is published to PSGallery - see RequiredModules.psd1, which pins the
    # same version for build-time dependency resolution.
    @{ ModuleName = 'GraphKit'; ModuleVersion = '0.1.1' }
)

# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()

# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()

# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()

# Format files (.ps1xml) to be loaded when importing this module
# FormatsToProcess = @()

# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
# NestedModules = @()

# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @('Export-PulseReport','Get-PulseCheckCatalog','Get-PulseTenantSnapshot','Invoke-PulseAssessment','Invoke-PulseCheck')

# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = @()

# Variables to export from this module
VariablesToExport = @()

# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = @()

# DSC resources to export from this module
DscResourcesToExport = @()

# List of all modules packaged with this module
# ModuleList = @()

# List of all files packaged with this module
# FileList = @()

# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{

    PSData = @{

        # Tags applied to this module. These help with module discovery in online galleries.
        Tags = @('Intune', 'Entra', 'TenantHealth', 'Graph')

        # A URL to the license for this module.
        LicenseUri = 'https://github.com/AdamGell/TenantPulse/blob/main/LICENSE'

        # A URL to the main website for this project.
        ProjectUri = 'https://github.com/AdamGell/TenantPulse'

        # A URL to an icon representing this module.
        # IconUri = ''

        # ReleaseNotes of this module
        ReleaseNotes = '## [0.1.0] - 2026-08-18

### Added

- Repository scaffold: Sampler build pipeline, module manifest, QA gates, and CI workflow.
- Snapshot store: `New-PulseSnapshotStore`, `Write-PulseDataset`, `Read-PulseDataset`,
  `Get-PulseSnapshotManifest`, `Set-PulseManifestEntry`, and the canonical serialization
  primitive `ConvertTo-PulseCanonicalJson`, with a hashed, reason-carrying manifest that
  every later collector/evaluator/renderer component builds on.
- Operator key + pseudonymization: `Get-PulseOperatorKey` auto-creates a 32-byte key
  (0600-equivalent permissions on non-Windows) at `~/.tenantpulse/operator.key` on first
  call and refuses to create or read one inside a snapshot root; `Get-PulsePseudonym`
  turns any value into a stable `tp-`-prefixed HMAC-SHA256 pseudonym under that key, so
  raw tenant IDs never need to appear in snapshots or reports.
- Check descriptor schema + loader: `Import-PulseCheckCatalog` reads and validates every
  `.psd1` check descriptor under a directory (default `source/Data/Checks`) with
  `Import-PowerShellDataFile`, returning an ordinally-sorted-by-`Id` array of
  `TenantPulse.CheckDescriptor` objects; an empty/missing catalog returns an empty array
  rather than throwing. `Test-PulseCheckDescriptor` validates one descriptor''s schema
  with explicit per-field type enforcement (scalar `[string]` vs. required/allowed-empty
  `[string[]]`, reported as `must be a <type>, got <actual type>` rather than silently
  coercing), Id/Severity/Effort/Impact/Rule.Type patterns and enums, Rule.Function
  resolving via `Get-Command` at import time, required Consulting fields,
  References.Research/Authorities and, once Task 1.5 lands `DatasetMap.psd1`,
  cross-checks Data.Datasets membership against a map parsed exactly once per catalog
  load. All validation errors across a catalog are aggregated into a single thrown
  error, each line prefixed with its source filename and naming the offending
  descriptor and property, rather than failing on the first bad file. Schema documented
  verbatim in `source/Data/Checks/README.md`.
- Evaluator: `Invoke-PulseEvaluation` runs a check catalog against a snapshot store and
  produces the canonical findings document (`source/Private/Evaluate/FindingsSchema.md`),
  plus an in-memory-only `RedactionMap` covering every evidence identity seen (built at
  evaluate, applied at render). `New-PulseFinding` is the rule-result contract every
  Function rule (`Test-Pulse* -Datasets <hashtable>`) returns to produce `Pass`/`Warn`/
  `Fail` with evidence; Expression rules resolve straight to `[bool]` with no evidence.
  `NotApplicable` and `Error` are engine-assigned only: a missing/`Failed`/`Skipped`
  dataset degrades a check to `NotApplicable` quoting the manifest''s own (already
  redacted) reason verbatim, and a throwing or malformed-output rule becomes `Error`
  without aborting the rest of the run. `Get-PulseGateStatus` is a Phase 1 gate stub -
  every gate resolves `Unknown`, which never degrades a check. Findings are sorted
  ordinally by check Id, each finding''s evidence ordinally by sortKey then identity, and
  `generatedUtc` is pinned to the snapshot manifest''s own `createdUtc` (never wall
  clock), so re-evaluating the same snapshot is byte-identical through
  `ConvertTo-PulseCanonicalJson`.
- **Settings expansion (Phase 2, `-ExpandSettings`)**: `Get-PulseTenantSnapshot
  -ExpandSettings` (and `Invoke-PulseAssessment -ExpandSettings`, a straight pass-through)
  decomposes every policy TenantPulse can currently setting-expand into individual
  canonical setting rows, on top of Phase 1''s ordinary check-driven collection. Two typed
  expansion drivers cover two different policy shapes: `Invoke-PulseSettingsCatalogExpansion`
  walks the modern, definitionId-driven Settings Catalog (`configurationPolicies` +
  `ConfigurationPolicySetting.ListBeta` per policy); `Invoke-PulseTypedPolicyExpansion`
  covers compliance and legacy device configuration policies (`deviceCompliancePolicies`,
  `deviceConfigurations`), the older polymorphic `@odata.type`-typed Graph resources,
  decomposed via a hand-maintained property map (`source/Data/TypedPolicyMaps.psd1`) since
  no Graph-side settings catalog exists for them. `-ExpandSettings` is **default-off,
  deliberately**: the live gate against the Ivy24 lab tenant passed clean end to end, but
  actually flipping the default surfaced two real, wider-blast-radius costs (a
  `PSAvoidDefaultValueSwitchParameter` QA-gate lint failure, and at least two existing
  `Get-PulseTenantSnapshot` unit tests asserting on the manifest shape a default-on flip
  changes for every caller, not just opt-in ones) not appropriate to absorb under this same
  task''s time budget - flipping the default is real, scoped follow-up work, not done here.
  Every expansion artifact is recorded in `manifest.expansions.<name>` and written to an
  immutable, content-addressed file, never a fixed name: three row-producer "families"
  (`settingsCatalog`, `compliance`, `deviceConfiguration`) each publish
  `expanded/<name>.<sha256>.jsonl` (one canonical JSON object per line, row-schema v1), and
  a single conflict-detection pass over every row from all three families publishes
  `expanded/conflicts.<sha256>.json` - one JSON document naming every `settingDefinitionId`
  that two or more policies disagree on, each with a four-state `assignmentOverlap` verdict
  (`proven`/`possible`/`none`/`unknown`; see `FindingsSchema.md`''s "Settings expansion
  artifacts" section for the exact schema). **The assignments-deferred split**: Settings
  Catalog rows carry `assignments: null` in this core slice because
  `ConfigurationPolicyAssignment.ListBeta` (the descriptor that would resolve which
  devices/users a Settings Catalog policy actually targets) is not yet a released GraphKit
  descriptor - any conflict involving such a row reports `assignmentOverlap: ''unknown''`
  with an explicit `''assignments-deferred: awaiting GraphKit release''` reason rather than a
  silently wrong verdict. Compliance and legacy device configuration policies do NOT have
  this gap - their assignment descriptors (`DeviceCompliancePolicyAssignment.List`/
  `DeviceConfigurationAssignment.List`) already shipped in GraphKit 0.1.1, so their rows
  carry real assignment data today. Closing the Settings Catalog side is tracked as its own
  follow-up slice ("Phase 2b"), not a silent limitation. A dedicated, serial perf container
  (`tests/Perf/ScaleAndMemory.Tests.ps1`, run via `./build.ps1 -Tasks build,perftest`, never
  part of the default test workflow) measures and budgets, at `[measured] x1.5` headroom, a
  5,000-policy synthetic Settings Catalog expansion + conflict-detection compute pass
  (mocked Graph), a 50,000-row `managedDevices` write+read memory ceiling, and raw
  per-policy dataset write scaling - see `docs/spike/2026-08-16-t27-perf-container.md` for
  the full recorded numbers, hardware, and method, and `docs/STATUS.md`''s own scale section
  for the documented gaps it surfaced. **Live-gated against the Ivy24 lab tenant end to
  end**: 781 Settings Catalog policies (`Partial`, 64 per-instance gaps, 0%
  unresolved-name rate, 131 redacted secret values), 40 compliance policies (`Partial`, 6
  gapped on an unmapped `@odata.type`), 15 device configuration policies (`Expanded`, zero
  gaps, 8 redacted secret values), and conflict detection surfacing **165 real conflict
  entries** across all three families (`assignmentOverlap` breakdown: `none`=8,
  `possible`=34, `unknown`=123, the 123 all involving at least one assignments-deferred
  Settings Catalog row) - not a zero-conflicts-by-luck outcome. `-FromSnapshot` re-derived
  all four expansion artifacts byte-identical to the original run.

### Changed

- Check descriptor loader hardening (post-review): closed PowerShell-coercion type holes
  that let array-typed `Id`/`Severity`/`Rule.Type`/etc. silently pass their
  pattern/enum checks and land wrong-typed in the loaded descriptor (also closing the
  matching duplicate-Id-detection bypass); prefixed every aggregated error line with its
  source filename so two files sharing a bad Id are distinguishable; hoisted
  `DatasetMap.psd1` parsing to once per catalog load with its own aggregated-error
  handling for a malformed map instead of a raw exception, and excluded a
  `DatasetMap.psd1` living inside the catalog directory from descriptor scanning;
  switched catalog ordering from culture-aware `Sort-Object` to an ordinal
  `[string]::CompareOrdinal` index-sort, matching `ConvertTo-PulseCanonicalJson`''s
  established pattern for deterministic ordering.
- **Final fix-wave, contract:** `Get-PulseTenantSnapshot`''s snapshot manifest `tenant`
  pseudonym is now derived from the resolved GraphKit tenant id (`$Context.TenantId`),
  never from `-ProfileId` - the same tenant under two differently-named profiles now
  pseudonymizes identically, and renaming a profile no longer changes the pseudonym.
  `Get-GraphContext` is now called before the snapshot store is created so the tenant id
  is available before anything is written. `Get-PulseTenantSnapshot -Path` and
  `Get-PulseCheckCatalog -Path` are renamed to `-OutputPath`/`-CatalogPath` respectively
  (`-Path` kept as a deprecated alias for one release). `Export-PulseReport`''s return
  object now carries `FindingsPath`, matching `Invoke-PulseAssessment`''s return shape.
- **Final fix-wave, snapshot-store boundary:** `New-PulseSnapshotStore` now clears
  `datasets/`, `reference/` and `expanded/` before reuse (unconditionally, not behind a
  `-Force` flag), so a prior run''s or a foreign file left in an existing store path can
  no longer silently survive into a new run. `Get-PulseSnapshotStore` now type-validates
  `datasets` (non-null object), `createdUtc` (a parseable timestamp) and `producer`
  (non-null object) when opening a store, throwing a specific path+field error instead of
  producing a confident-but-wrong evaluation or a mid-run crash. `Write-Pulse'


        # Prerelease string of this module
        Prerelease =  ''

        # Flag to indicate whether the module requires explicit user acceptance for install/update/save
        # RequireLicenseAcceptance = $false

        # External dependent modules of this module
        # ExternalModuleDependencies = @()

    } # End of PSData hashtable

} # End of PrivateData hashtable

# HelpInfo URI of this module
# HelpInfoURI = ''

# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
# DefaultCommandPrefix = ''

}