modules/FileShares/FileShares.psm1
|
<#
FileShares.psm1 - SMB shares, permissions, optional deep crawl, DFS (read-only). Produces: SmbShares, SharePermissions, NtfsAclSummary, FileShareSummary, DfsDiscovery. Fast mode: no crawl. Deep / -DeepFileShareScan: bounded folder/size/age analysis. #> function Get-DiscoveryModuleMetadata { [pscustomobject]@{ ModuleName='FileShares'; DisplayName='File Shares & DFS'; Category='File'; Version='1.0.0' DefaultInFast=$true; DefaultInDeep=$true; RequiresAdmin=$false; RequiresDomainContext=$false RequiresRole=$null; EstimatedImpact='Low'; CanRunAsSystem=$true ProducesDatasets=@('SmbShares','SharePermissions','NtfsAclSummary','FileShareSummary','DfsDiscovery') ProducesRisks=$true; ProducesFollowUpQuestions=$false; SupportsDeepMode=$true; SupportsComplianceLens=$false } } function Test-DiscoveryPrerequisites { param([object]$Context) [pscustomobject]@{ ModuleName='FileShares'; CanRun=$true; Status='Ready'; Reason=''; Limitations=@() } } function Invoke-DiscoveryCollection { param([object]$Context) $shares = [System.Collections.Generic.List[object]]::new() $perms = [System.Collections.Generic.List[object]]::new() $acls = [System.Collections.Generic.List[object]]::new() $dfs = [System.Collections.Generic.List[object]]::new() $deep = ([bool]$Context.Parameters['DeepFileShareScan']) # ---- Shares ---- try { if (Get-CommandAvailable -Name 'Get-SmbShare') { foreach ($s in (Get-SmbShare -ErrorAction SilentlyContinue)) { $isUser = (-not ($s.Name.EndsWith('$'))) -and ([string]$s.ShareType -eq 'FileSystemDirectory') # not print queues / IPC / devices $shares.Add([pscustomobject]@{ Name=$s.Name; Path=$s.Path; Description=$s.Description; ShareType=[string]$s.ShareType; IsUserShare=$isUser; FolderEnumerationMode=[string]$s.FolderEnumerationMode; EncryptData=$s.EncryptData }) if ($isUser) { try { foreach ($a in (Get-SmbShareAccess -Name $s.Name -ErrorAction SilentlyContinue)) { $perms.Add([pscustomobject]@{ Share=$s.Name; Account=$a.AccountName; AccessRight=[string]$a.AccessRight; AccessType=[string]$a.AccessControlType }) } } catch { } } } } else { foreach ($s in (Invoke-CimSafe -ClassName 'Win32_Share')) { $isUser = (-not ($s.Name.EndsWith('$'))) -and (([uint32]$s.Type -band 0x7FFFFFFF) -eq 0) # Win32_Share Type 0 = disk share $shares.Add([pscustomobject]@{ Name=$s.Name; Path=$s.Path; Description=$s.Description; ShareType=[string]$s.Type; IsUserShare=$isUser; FolderEnumerationMode=''; EncryptData=$null }) } } } catch { Add-Limitation -Context $Context -Module 'FileShares' -Message 'Share enumeration failed.' -Reason $_.Exception.Message | Out-Null } # ---- Deep crawl (only when enabled) ---- if ($deep) { $maxDepth = [int]$Context.Parameters['MaxDepth']; if ($maxDepth -le 0) { $maxDepth = 3 } $largeGB = [double]$Context.Parameters['LargeFileThresholdGB']; if ($largeGB -le 0) { $largeGB = 5 } $oldYears = [int]$Context.Parameters['OldFileYears']; if ($oldYears -le 0) { $oldYears = 7 } $cutoff = (Get-Date).AddYears(-$oldYears) # Recycle-bin contents are excluded from share size/age totals by default: deleted # data is not migration payload, and counting it silently inflates the data-volume # estimate a quote is built from. -IncludeRecycleBin folds it back in. Either way the # recycled volume is reported separately, since it is reclaimable space worth knowing. $includeRecycleBin = ([bool]$Context.Parameters['IncludeRecycleBin']) $recyclePattern = '(?i)[\\/]\$Recycle\.Bin[\\/]' foreach ($s in ($shares | Where-Object { $_.IsUserShare -and $_.Path -and (Test-Path -LiteralPath $_.Path) })) { try { $topFolders = @(Get-ChildItem -LiteralPath $s.Path -Directory -ErrorAction SilentlyContinue) $allFiles = @(Get-ChildItem -LiteralPath $s.Path -File -Recurse -Force -Depth ($maxDepth) -ErrorAction SilentlyContinue) # -Force: $RECYCLE.BIN is Hidden+System, invisible without it $recycled = @($allFiles | Where-Object { $_.FullName -match $recyclePattern }) $files = @(if ($includeRecycleBin) { $allFiles } else { @($allFiles | Where-Object { $_.FullName -notmatch $recyclePattern }) }) $totalBytes = ($files | Measure-Object -Property Length -Sum).Sum $recycledBytes = ($recycled | Measure-Object -Property Length -Sum).Sum $largeFiles = @($files | Where-Object { $_.Length -ge ($largeGB * 1GB) }).Count $oldFiles = @($files | Where-Object { $_.LastWriteTime -lt $cutoff }).Count $recent = @($files | Where-Object { $_.LastWriteTime -ge (Get-Date).AddDays(-30) }).Count $acls.Add([pscustomobject]@{ Share=$s.Name; Path=$s.Path; TopLevelFolders=$topFolders.Count FileCount=$files.Count; TotalSizeGB=(Convert-BytesToGB $totalBytes) LargeFilesOverThreshold=$largeFiles; OldFilesOverYears=$oldFiles; RecentlyModified30d=$recent; MaxDepthScanned=$maxDepth RecycleBinIncluded=$includeRecycleBin; RecycleBinFilesFound=$recycled.Count; RecycleBinSizeGB=(Convert-BytesToGB $recycledBytes) }) } catch { Add-Limitation -Context $Context -Module 'FileShares' -Message ("Deep scan of share '{0}' failed or was partial." -f $s.Name) -Reason $_.Exception.Message | Out-Null } } } # ---- DFS ---- try { if (Get-CommandAvailable -Name 'Get-DfsnRoot') { foreach ($r in (Get-DfsnRoot -ErrorAction SilentlyContinue)) { $dfs.Add([pscustomobject]@{ Type='Namespace'; Name=$r.Path; Detail=[string]$r.State }) } } if (Get-CommandAvailable -Name 'Get-DfsReplicationGroup') { foreach ($g in (Get-DfsReplicationGroup -ErrorAction SilentlyContinue)) { $dfs.Add([pscustomobject]@{ Type='Replication'; Name=$g.GroupName; Detail=$g.Description }) } } $dfsr = Get-Service -Name 'DFSR' -ErrorAction SilentlyContinue if ($dfsr) { $dfs.Add([pscustomobject]@{ Type='Service'; Name='DFSR'; Detail=[string]$dfsr.Status }) } } catch { } $summary = @([pscustomobject]@{ DeepScanPerformed=$deep ShareCount=@($shares).Count UserShareCount=@($shares | Where-Object { $_.IsUserShare }).Count DeepScannedShares=@($acls).Count }) return ,@{ SmbShares=@($shares); SharePermissions=@($perms); NtfsAclSummary=@($acls); FileShareSummary=$summary; DfsDiscovery=@($dfs) } } function ConvertTo-DiscoveryDatasets { param([object]$Context, $RawData) if (-not $RawData) { $RawData = @{} } $get = { param($k) if ($RawData[$k]) { @($RawData[$k]) } else { @() } } Add-DataSet -Context $Context -Name 'SmbShares' -Description 'SMB shares (user vs administrative).' -Rows (& $get 'SmbShares') -Visibility 'Both' -SourceModule 'FileShares' | Out-Null Add-DataSet -Context $Context -Name 'SharePermissions' -Description 'Share-level permissions for user shares.' -Rows (& $get 'SharePermissions')-Visibility 'Internal' -SourceModule 'FileShares' | Out-Null Add-DataSet -Context $Context -Name 'NtfsAclSummary' -Description 'Deep-scan folder/size/age summary per share.' -Rows (& $get 'NtfsAclSummary') -Visibility 'Internal' -SourceModule 'FileShares' | Out-Null Add-DataSet -Context $Context -Name 'FileShareSummary' -Description 'File share scan summary (DeepScanPerformed).' -Rows (& $get 'FileShareSummary')-Visibility 'Both' -SourceModule 'FileShares' | Out-Null Add-DataSet -Context $Context -Name 'DfsDiscovery' -Description 'DFS namespace/replication indicators.' -Rows (& $get 'DfsDiscovery') -Visibility 'Internal' -SourceModule 'FileShares' | Out-Null # Dependency edges for user shares foreach ($s in (& $get 'SmbShares')) { if ($s.IsUserShare) { Add-DependencyEdge -Context $Context -SourceType 'SmbShare' -SourceName $s.Name -DependencyType 'ServesPath' -Target $s.Path -Evidence 'Shared folder' -Confidence 'Confirmed' -SourceDataset 'SmbShares' -ProjectImpact 'Data Migration' -ValidationQuestion 'Who uses this share and is it referenced by server name/UNC?' | Out-Null } } } Export-ModuleMember -Function 'Get-DiscoveryModuleMetadata','Test-DiscoveryPrerequisites','Invoke-DiscoveryCollection','ConvertTo-DiscoveryDatasets' |