modules/normalizers/Normalize-ADORepoSecrets.ps1
|
#Requires -Version 7.4 [CmdletBinding()] param () . "$PSScriptRoot\..\shared\Schema.ps1" . "$PSScriptRoot\..\shared\Canonicalize.ps1" function Normalize-ADORepoSecrets { [CmdletBinding()] param ( [Parameter(Mandatory)] [PSCustomObject] $ToolResult ) if ($ToolResult.Status -notin @('Success', 'PartialSuccess') -or -not $ToolResult.Findings) { return @() } $runId = [guid]::NewGuid().ToString() $normalized = [System.Collections.Generic.List[PSCustomObject]]::new() function Convert-ConfidenceValue { param ([string] $Value) switch -Regex (($Value ?? '').Trim()) { '^(?i)confirmed$' { return 'Confirmed' } '^(?i)likely$' { return 'Likely' } '^(?i)unconfirmed$' { return 'Unconfirmed' } '^(?i)unknown|n/a$' { return 'Unknown' } default { return 'Unknown' } } } function Get-ConfidenceTierTag { param ([string] $ConfidenceValue) switch ($ConfidenceValue) { 'Confirmed' { return 'high' } 'Likely' { return 'medium' } default { return 'low' } } } function Get-ProviderRotationLink { param ([string] $SecretType) $rule = ($SecretType ?? '').ToLowerInvariant() if ($rule -match 'aws') { return 'https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_RotateAccessKey' } if ($rule -match 'azure.*storage') { return 'https://learn.microsoft.com/azure/storage/common/storage-account-keys-manage' } if ($rule -match 'github.*pat|pat') { return 'https://github.com/settings/personal-access-tokens' } return 'https://learn.microsoft.com/azure/devops/repos/security/secret-scanning?view=azure-devops' } function Get-ImpactValue { param ( [string] $SecretType, [string] $ConfidenceValue, [bool] $Compliant ) if ($Compliant) { return 'Low' } $rule = ($SecretType ?? '').ToLowerInvariant() $isCloudCredential = $rule -match 'aws-access-key|azure-storage-key|azure-client-secret|github-pat|private-key' $isGeneric = $rule -match 'generic' if ($ConfidenceValue -eq 'Confirmed' -and $isCloudCredential) { return 'Critical' } if ($ConfidenceValue -eq 'Confirmed') { return 'High' } if ($isGeneric) { return 'Medium' } if ($ConfidenceValue -eq 'Likely' -and $isCloudCredential) { return 'High' } return 'Medium' } function Get-EffortValue { param ( [string] $SecretType, [string] $ConfidenceValue ) $rule = ($SecretType ?? '').ToLowerInvariant() if ($ConfidenceValue -eq 'Confirmed' -and $rule -match 'aws-access-key|azure-storage-key|github-pat|private-key') { return 'High' } return 'Medium' } function Build-AdoRepoBlobLink { param ( [string] $Org, [string] $Project, [string] $Repo, [string] $FilePath, [string] $CommitSha, [int] $LineNumber ) if ([string]::IsNullOrWhiteSpace($Org) -or [string]::IsNullOrWhiteSpace($Project) -or [string]::IsNullOrWhiteSpace($Repo) -or [string]::IsNullOrWhiteSpace($FilePath) -or [string]::IsNullOrWhiteSpace($CommitSha)) { return '' } $normalizedPath = if ($FilePath.StartsWith('/')) { $FilePath } else { "/$FilePath" } $link = "https://dev.azure.com/$([uri]::EscapeDataString($Org))/$([uri]::EscapeDataString($Project))/_git/$([uri]::EscapeDataString($Repo))?path=$([uri]::EscapeDataString($normalizedPath))&version=GC$([uri]::EscapeDataString($CommitSha))" if ($LineNumber -gt 0) { $link += "&line=$LineNumber" } return $link } foreach ($finding in $ToolResult.Findings) { $repoIdRaw = if ($finding.PSObject.Properties['RepositoryCanonicalId'] -and $finding.RepositoryCanonicalId) { [string]$finding.RepositoryCanonicalId } else { 'ado://unknown/unknown/repository/unknown' } $entityId = '' try { $entityId = (ConvertTo-CanonicalEntityId -RawId $repoIdRaw -EntityType 'Repository').CanonicalId } catch { $entityId = $repoIdRaw.ToLowerInvariant() } $severity = switch -Regex ([string]$finding.Severity) { '^(?i)critical$' { 'Critical' } '^(?i)high$' { 'High' } '^(?i)medium$' { 'Medium' } '^(?i)low$' { 'Low' } '^(?i)info$' { 'Info' } default { 'Info' } } $resourceId = '' if ($finding.PSObject.Properties['FilePath'] -and $finding.FilePath) { $resourceId = ([string]$finding.FilePath).Trim().ToLowerInvariant() -replace '\\', '/' } $rawConfidence = if ($finding.PSObject.Properties['Confidence'] -and $finding.Confidence) { [string]$finding.Confidence } else { 'Unknown' } $confidence = Convert-ConfidenceValue -Value $rawConfidence $confidenceTier = Get-ConfidenceTierTag -ConfidenceValue $confidence $secretType = if ($finding.PSObject.Properties['SecretType'] -and $finding.SecretType) { [string]$finding.SecretType } elseif ($finding.PSObject.Properties['RuleId'] -and $finding.RuleId) { [string]$finding.RuleId } else { 'unknown-rule' } $ruleId = $secretType $commitSha = if ($finding.PSObject.Properties['CommitSha'] -and $finding.CommitSha) { [string]$finding.CommitSha } else { '' } $lineNumber = if ($finding.PSObject.Properties['LineNumber'] -and $finding.LineNumber) { [int]$finding.LineNumber } elseif ($finding.PSObject.Properties['StartLine'] -and $finding.StartLine) { [int]$finding.StartLine } else { 0 } if ($lineNumber -le 0 -and $finding.PSObject.Properties['Detail'] -and $finding.Detail -match '(?i)\bline\s+(\d+)\b') { $lineNumber = [int]$Matches[1] } $adoOrg = if ($finding.PSObject.Properties['AdoOrg'] -and $finding.AdoOrg) { [string]$finding.AdoOrg } else { '' } $adoProject = if ($finding.PSObject.Properties['AdoProject'] -and $finding.AdoProject) { [string]$finding.AdoProject } else { '' } $repoName = if ($finding.PSObject.Properties['RepositoryName'] -and $finding.RepositoryName) { [string]$finding.RepositoryName } else { '' } $commitUrl = if ($finding.PSObject.Properties['CommitUrl'] -and $finding.CommitUrl) { [string]$finding.CommitUrl } elseif ($adoOrg -and $adoProject -and $repoName -and $commitSha) { "https://dev.azure.com/$([uri]::EscapeDataString($adoOrg))/$([uri]::EscapeDataString($adoProject))/_git/$([uri]::EscapeDataString($repoName))/commit/$([uri]::EscapeDataString($commitSha))" } else { '' } $blobUrl = if ($finding.PSObject.Properties['BlobUrl'] -and $finding.BlobUrl) { [string]$finding.BlobUrl } else { Build-AdoRepoBlobLink -Org $adoOrg -Project $adoProject -Repo $repoName -FilePath $resourceId -CommitSha $commitSha -LineNumber 0 } $deepLinkUrl = if ($finding.PSObject.Properties['DeepLinkUrl'] -and $finding.DeepLinkUrl) { [string]$finding.DeepLinkUrl } else { Build-AdoRepoBlobLink -Org $adoOrg -Project $adoProject -Repo $repoName -FilePath $resourceId -CommitSha $commitSha -LineNumber $lineNumber } $scannerArtifactPath = if ($finding.PSObject.Properties['ScannerArtifactPath'] -and $finding.ScannerArtifactPath) { [string]$finding.ScannerArtifactPath } else { '' } $evidenceUris = [System.Collections.Generic.List[string]]::new() foreach ($candidate in @($commitUrl, $blobUrl, $scannerArtifactPath)) { if (-not [string]::IsNullOrWhiteSpace($candidate) -and -not $evidenceUris.Contains($candidate)) { $evidenceUris.Add($candidate) | Out-Null } } $impact = Get-ImpactValue -SecretType $secretType -ConfidenceValue $confidence -Compliant ([bool]$finding.Compliant) $effort = Get-EffortValue -SecretType $secretType -ConfidenceValue $confidence $providerRotationLink = Get-ProviderRotationLink -SecretType $secretType $remediationGuidance = @( 'Revoke the exposed secret at the provider immediately.' 'Rotate the credential and redeploy consumers with the new value.' 'Rewrite git history to remove the secret (for example git filter-repo), then force-push cleaned history.' "Provider rotation reference: $providerRotationLink" ) -join "`n" $remediationSnippets = @( @{ language = 'text' code = $remediationGuidance } ) $toolVersion = if ($finding.PSObject.Properties['ToolVersion'] -and $finding.ToolVersion) { [string]$finding.ToolVersion } else { '' } $title = if ($resourceId -and $lineNumber -gt 0) { "$secretType in ${resourceId}:$lineNumber" } elseif ($resourceId) { "$secretType in $resourceId" } else { [string]$finding.Title } $baselineTags = @($secretType, $confidenceTier, "ruleId:$ruleId") $entityRefs = [System.Collections.Generic.List[string]]::new() $entityRefs.Add($entityId) | Out-Null if (-not [string]::IsNullOrWhiteSpace($commitSha)) { $entityRefs.Add("commit:$commitSha") | Out-Null } $row = New-FindingRow -Id ([string]$finding.Id) ` -Source 'ado-repos-secrets' -EntityId $entityId -EntityType 'Repository' ` -Title $title -RuleId $ruleId -Compliant ([bool]$finding.Compliant) -ProvenanceRunId $runId ` -Platform 'ADO' -Category 'Secret Detection' -Severity $severity ` -Detail ([string]$finding.Detail) -Remediation ([string]$finding.Remediation) ` -LearnMoreUrl ([string]$finding.LearnMoreUrl) -ResourceId $resourceId ` -Confidence $confidence -Pillar 'Security' -Impact $impact -Effort $effort ` -DeepLinkUrl $deepLinkUrl -RemediationSnippets $remediationSnippets ` -EvidenceUris @($evidenceUris) -BaselineTags $baselineTags ` -EntityRefs @($entityRefs) -ToolVersion $toolVersion if ($null -ne $row) { $normalized.Add($row) } } return @($normalized) } |