Skip to content

feat(rules): Get-Credential -Credential is a note when the argument can only be a credential - #13

Merged
fadwen merged 1 commit into
test/ps7-tables-against-hostsfrom
feat/get-credential-precision
Oct 6, 2026
Merged

fadwen merged 1 commit into
test/ps7-tables-against-hostsfrom
feat/get-credential-precision

Conversation

@fadwen

@fadwen fadwen commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Summary

IslInteractiveCall warned on every Get-Credential -Credential call handed anything but a literal, because the argument may hold a user name, which prompts, or a credential that is already built, which Get-Credential returns as it is (REM-CRED-BUILT, 12 ms under the agent). The rule now follows the argument back through the script. When every value it can take is a PSCredential, the finding is an Information note that says the call can go; the warning stays for anything the script leaves open.

Stacked on #12 (base branch), which is stacked on #11 and #10; the diff is this change only. Merge #10, #11, #12, then this; GitHub retargets each to main as its parent merges and the checks run then.

Changes

  • Private/Rules/Find-IslInteractiveCall.ps1. A value counts as a credential when it is built by [pscredential]::new() or New-Object with the PSCredential type, cast to the type, or read by Import-Clixml, which hands back what Export-Clixml wrote, a credential in this idiom. A variable counts when every assignment to it, and any parameter of its name, is one of those or is typed [pscredential]. A second assignment of another kind, an untyped parameter, a pipeline, a call or a member expression keeps the warning. The new finding cites the same evidence as the warning: the device measurement is the same, the refinement is static.
  • docs/Rules.md regenerated with the Information row. README and Findings say where the note applies.
  • Tests. Twelve scripts that earn the note, from the constructor through a typed parameter and a scope-qualified variable to a parenthesized Import-Clixml; eight that keep the warning. The parenthesized Import-Clixml case, a warning before, moved to the note set.

Verification

  • Unit and integration suites: 958 pass on PowerShell 7.6.6 (8 skipped: elevation, lab credential); the rule's tests and the rule reference test pass on Windows PowerShell 5.1 (40 of 40). PSScriptAnalyzer (Error and Warning) is clean; no line over 115 characters; help Markdown validates; docs/Rules.md regenerates unchanged.
  • The shipped example scripts have no Get-Credential call, so no example's verdict changes.

@fadwen fadwen left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notes on the lines whose reason the diff does not show.

}
}

$credentialType = '^(System\.Management\.Automation\.)?PSCredential$'

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeName.FullName is the type as the script wrote it, pscredential, PSCredential or the namespaced name; -match is case-insensitive, so all three pass. The accelerator is not resolved, which keeps the rule off reflection and on the AST alone like the rest of the rules.

function Get-CredentialSource {
param($Expression)
# Parentheses, a one-element pipeline and the expression statement around a value are wrappers
$unwrapped = $false

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A flag rather than a switch with continue: inside a switch, continue applies to the switch, not to the enclosing while, so the first draft unwrapped one layer only and a parenthesized Import-Clixml stayed a warning.

for ($index = 0; $index -lt $elements.Count; $index++) {
$element = $elements[$index]
if ($element.GetType().Name -eq 'CommandParameterAst') {
if (-not 'TypeName'.StartsWith($element.ParameterName, 'OrdinalIgnoreCase')) { continue }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefix match as PowerShell binds parameters: -Type and -TypeN name the same parameter. -ComObject never matches, so New-Object -ComObject is not taken for a credential.

-not $previous.Argument
if (-not $taken) { $typeName = $element.Extent.Text; break }
}
if ("$typeName".Trim('''"') -match $credentialType) { return 'New-Object PSCredential' }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type can be written quoted, New-Object 'System.Management.Automation.PSCredential', and Extent.Text keeps the quotes.

# never gives it a value, or any one of them could be something else
function Get-VariableCredentialSource {
param($Variable)
$scopePrefix = '^(script|local|private|global):'

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$script:c and $c name the same variable in a script body, so the prefix is dropped on both sides. Scopes of nested functions are not modelled: every assignment to the name anywhere in the script counts, which can only keep the warning, never add the note.

@{ Call = 'Get-Credential -Cred:$built'; Handed = '$built' }
@{ Call = 'Get-Credential -ErrorAction Stop -Credential $settings.Account'; Handed = '$settings.Account' }
@{ Call = 'Get-Credential (Import-Clixml C:\x.xml)'; Handed = '(Import-Clixml C:\x.xml)' }
@{ Call = 'Get-Credential (Get-Thing)'; Handed = '(Get-Thing)' }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parenthesized Import-Clixml case this replaces is now a note, and sits in the set below. A call the rule knows nothing about keeps this case a warning.

@fadwen
fadwen added this pull request to stack #15 October 6, 2026 06:46
…an only be a credential

IslInteractiveCall warned on every Get-Credential -Credential call handed
anything but a literal, because a variable there may hold a user name
(prompts) or a credential that is already built (returned as it is,
REM-CRED-BUILT). The rule now follows the argument back through the
script: when every value it can take is a PSCredential, the finding is
an Information note that says the call can go.

A value counts as a credential when it is built by [pscredential]::new()
or New-Object with the PSCredential type, cast to the type, read by
Import-Clixml (which hands back what Export-Clixml wrote, a credential
in this idiom), or held by a variable or parameter typed [pscredential].
A variable qualifies when every assignment to it, and any parameter of
that name, is one of those; a second assignment of another kind, an
untyped parameter, a pipeline or a call keeps the warning, as does a
member expression.

The evidence is unchanged: the device measurement is the same, the
refinement is static.
@fadwen
fadwen force-pushed the feat/get-credential-precision branch from b1ce736 to 9a2275b Compare October 6, 2026 06:52
@fadwen
fadwen merged commit f20cfe5 into main Oct 6, 2026
4 checks passed
@fadwen
fadwen deleted the feat/get-credential-precision branch October 6, 2026 06:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant