Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions Private/MiniShaiHulud/Find-MshSuspiciousScripts.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,19 @@ function Find-MshSuspiciousScripts {
if (-not $pkg) { continue }

# Defensive: ConvertFrom-Json on a top-level scalar (e.g. just `42`
# or `"hello"`) returns a primitive, not a PSCustomObject. Under
# StrictMode Latest, $primitive.PSObject.Properties.Name throws
# because the empty PSMemberInfoCollection doesn't expose .Name as
# a direct property and the strict-mode property-fallback fails.
# Hard-gate on PSCustomObject to avoid the entire class of failures.
# or `"hello"`) returns a primitive, not a PSCustomObject. Hard-gate
# on PSCustomObject before any property access.
if (-not ($pkg -is [System.Management.Automation.PSCustomObject])) { continue }
if (-not ($pkg.PSObject.Properties.Name -contains 'scripts')) { continue }
# Use the PSObject.Properties indexer instead of the `.Name -contains`
# idiom. Under StrictMode Latest, the `.Name` and `.Count` members on
# an EMPTY PSMemberInfoCollection throw "The property 'Name' cannot
# be found on this object" — and the empty-collection case is hit
# routinely by published npm packages that ship `"scripts": {}` in
# their package.json. The indexer returns either the property or
# $null, regardless of whether the collection is empty. See
# Tests/MiniShaiHulud/Find-MshSuspiciousScripts.Tests.ps1 — the
# "empty scripts object" regression case.
if ($null -eq $pkg.PSObject.Properties['scripts']) { continue }
$scripts = $pkg.scripts
if (-not $scripts) { continue }
# 'scripts' value can legally be a string, array, or null in malformed
Expand All @@ -91,8 +97,9 @@ function Find-MshSuspiciousScripts {
if (-not ($scripts -is [System.Management.Automation.PSCustomObject])) { continue }

foreach ($hook in @('postinstall', 'preinstall', 'install')) {
if (-not ($scripts.PSObject.Properties.Name -contains $hook)) { continue }
$rawHook = $scripts.$hook
$hookProp = $scripts.PSObject.Properties[$hook]
if ($null -eq $hookProp) { continue }
$rawHook = $hookProp.Value
if ($null -eq $rawHook) { continue }
$script = [string]$rawHook
if ([string]::IsNullOrWhiteSpace($script)) { continue }
Expand All @@ -105,8 +112,9 @@ function Find-MshSuspiciousScripts {
$hasExec = ($script -match 'child_process') -or ($script -match '\bexec\b') -or ($script -match '\bspawn\b')
$severity = if ($hasDecode -and $hasExec) { 'Critical' } else { 'High' }

$pkgName = if ($pkg.PSObject.Properties.Name -contains 'name' -and $pkg.name) {
[string]$pkg.name
$nameProp = $pkg.PSObject.Properties['name']
$pkgName = if ($null -ne $nameProp -and $nameProp.Value) {
[string]$nameProp.Value
} else {
Split-Path (Split-Path $mf -Parent) -Leaf
}
Expand Down
20 changes: 20 additions & 0 deletions Tests/MiniShaiHulud/Find-MshSuspiciousScripts.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,26 @@ Describe 'Find-MshSuspiciousScripts — StrictMode resilience to malformed packa
{ Find-MshSuspiciousScripts -ProjectPath $script:proj -Iocs $script:iocs } | Should -Not -Throw
}

It 'does not throw when scripts is an empty object {} (real-world common case)' {
# Many published npm packages ship `"scripts": {}` in package.json
# (declared, no hooks). Under StrictMode Latest, accessing
# $scripts.PSObject.Properties.Name on an EMPTY PSMemberInfoCollection
# throws "The property 'Name' cannot be found on this object." This
# bug was killing Check 5 once per project for every fleet engineer
# running the scanner — fixed by switching to the PSObject.Properties
# indexer pattern which is safe on empty collections.
_Plant 'empty-scripts' '{"name":"x","version":"1.0.0","scripts":{}}'
{ Find-MshSuspiciousScripts -ProjectPath $script:proj -Iocs $script:iocs } | Should -Not -Throw
}

It 'does not throw when package.json itself is an empty object {}' {
# Edge-case sibling of the empty-scripts bug: $pkg.PSObject.Properties
# is empty so any `.Name` access would throw. Fixed by the same
# indexer-pattern switch.
_Plant 'empty-pkg' '{}'
{ Find-MshSuspiciousScripts -ProjectPath $script:proj -Iocs $script:iocs } | Should -Not -Throw
}

It 'does not throw when an individual hook value is null' {
_Plant 'hook-null' '{"name":"x","scripts":{"postinstall":null,"build":"webpack"}}'
{ Find-MshSuspiciousScripts -ProjectPath $script:proj -Iocs $script:iocs } | Should -Not -Throw
Expand Down