From 46e938df46d1bde785f081c301ec8091c0f3844c Mon Sep 17 00:00:00 2001 From: Gaurav Sharma <223556219+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:44:12 +0530 Subject: [PATCH 1/6] CHORE: add shift-left secret scanning to PR validation (test) adds a SecretScan job to the pr-validation pipeline that runs the 1ES Secret Scanning (SEC101/SPMI) engine on every PR, so credential leaks like SEC101/037 SqlLegacyCredentials are caught pre-merge instead of at the GitHub->ADO sync. includes a clearly-fake planted probe fixture to verify detection end-to-end; both the fixture and (pending decision) the job are test scaffolding. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/pr-validation-pipeline.yml | 20 ++++++++++++++++++++ tests/_secretscan_probe.txt | 9 +++++++++ 2 files changed, 29 insertions(+) create mode 100644 tests/_secretscan_probe.txt diff --git a/eng/pipelines/pr-validation-pipeline.yml b/eng/pipelines/pr-validation-pipeline.yml index 8cc7ea8e..a7b8ed93 100644 --- a/eng/pipelines/pr-validation-pipeline.yml +++ b/eng/pipelines/pr-validation-pipeline.yml @@ -7,6 +7,26 @@ trigger: - main jobs: +# Shift-left secret scanning: run the 1ES Secret Scanning (SEC101 / SPMI) engine +# on every PR so credential leaks (e.g. SEC101/037 SqlLegacyCredentials) are caught +# pre-merge, instead of surfacing later at the GitHub->ADO sync push-protection gate. +- job: SecretScan + displayName: 'Secret Scan (shift-left SEC101)' + pool: + vmImage: 'windows-latest' + steps: + - task: 1ESSecretScanning@1 + displayName: 'Run 1ES Secret Scanning' + inputs: + Action: 'analyze' + Target: '$(Build.SourcesDirectory)\*' + Rules: 'GetClean' + Recurse: true + - task: PostAnalysis@2 + displayName: 'Fail build if secrets detected' + inputs: + GdnBreakAllTools: true + - job: CodeQLAnalysis displayName: 'CodeQL Security Analysis' pool: diff --git a/tests/_secretscan_probe.txt b/tests/_secretscan_probe.txt new file mode 100644 index 00000000..62ac2db6 --- /dev/null +++ b/tests/_secretscan_probe.txt @@ -0,0 +1,9 @@ +SECRET-SCAN PROBE — NOT A REAL CREDENTIAL. DELETE BEFORE MERGE. + +This file exists only to verify that the shift-left secret-scanning stage in the +PR-validation pipeline catches the SEC101/037 "SqlLegacyCredentials" pattern +(realistic Azure SQL hostname + UID + PWD) at PR time, pre-sync. + +The value below is fabricated and points at a non-existent host: + +Server=tcp:contoso-probe-doesnotexist.database.windows.net,1433;Database=probe;UID=probeuser;PWD=Fake_NotARealSecret_2026;Encrypt=yes;TrustServerCertificate=no; From 28f18991f8fdec2bae7f3da7d5c7a4d48bd5e372 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma <223556219+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:17:34 +0530 Subject: [PATCH 2/6] CHORE: make secret scan actually block on real leaks (SARIF gate) the previous PostAnalysis@2 step did not fail the build on 1ESSecretScanning findings (its break filter does not cover the secret scanner, and SPMI emits warnings not errors) - so the planted probe was detected but the run stayed green. replace it with a SARIF gate script that parses the SEC101 findings, ignores the sample-credential paths already excluded by .config/CredScanSuppressions.json (tests/benchmarks/eng/OneBranchPipelines), and fails the build on any remaining leak in shipping source. move the test probe to repo root so it is treated as a real (non-sample) leak. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/_secretscan_probe.txt => _secretscan_probe.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/_secretscan_probe.txt => _secretscan_probe.txt (100%) diff --git a/tests/_secretscan_probe.txt b/_secretscan_probe.txt similarity index 100% rename from tests/_secretscan_probe.txt rename to _secretscan_probe.txt From 217289897a47dc00f9689ce3ef3f342af08f0f2d Mon Sep 17 00:00:00 2001 From: Gaurav Sharma <223556219+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:18:17 +0530 Subject: [PATCH 3/6] CHORE: add the secret-scan gate script and wire it into the pipeline previous commit only captured the probe move; the pipeline edit (swap PostAnalysis@2 for the PowerShell gate) and the gate script itself were missed. add them so the SecretScan job actually references an existing script. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/pr-validation-pipeline.yml | 7 +- eng/pipelines/scripts/secretscan-gate.ps1 | 78 +++++++++++++++++++++++ 2 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 eng/pipelines/scripts/secretscan-gate.ps1 diff --git a/eng/pipelines/pr-validation-pipeline.yml b/eng/pipelines/pr-validation-pipeline.yml index a7b8ed93..1c0eff72 100644 --- a/eng/pipelines/pr-validation-pipeline.yml +++ b/eng/pipelines/pr-validation-pipeline.yml @@ -22,10 +22,11 @@ jobs: Target: '$(Build.SourcesDirectory)\*' Rules: 'GetClean' Recurse: true - - task: PostAnalysis@2 - displayName: 'Fail build if secrets detected' + - task: PowerShell@2 + displayName: 'Fail build if secrets detected in shipping source' inputs: - GdnBreakAllTools: true + filePath: '$(Build.SourcesDirectory)/eng/pipelines/scripts/secretscan-gate.ps1' + pwsh: false - job: CodeQLAnalysis displayName: 'CodeQL Security Analysis' diff --git a/eng/pipelines/scripts/secretscan-gate.ps1 b/eng/pipelines/scripts/secretscan-gate.ps1 new file mode 100644 index 00000000..d6569622 --- /dev/null +++ b/eng/pipelines/scripts/secretscan-gate.ps1 @@ -0,0 +1,78 @@ +<# +.SYNOPSIS + Shift-left secret-scan gate. Fails the build when 1ESSecretScanning (SPMI / + SEC101) reports a credential leak in shipping source, while allowing the + sample credentials that legitimately live in test / benchmark / pipeline dirs. + +.DESCRIPTION + The 1ESSecretScanning task only emits SARIF *warnings*; on its own it never + fails the run, and PostAnalysis@2's break filter does not cover it. This + script reads the emitted SARIF, keeps SEC101* findings, drops anything under + the paths already excluded by .config/CredScanSuppressions.json, and exits 1 + if any real finding remains. Path policy is kept in sync with that file. +#> +param( + [string]$SourcesDir = $env:BUILD_SOURCESDIRECTORY, + [string]$SearchRoot = $env:AGENT_BUILDDIRECTORY +) + +$ErrorActionPreference = 'Stop' + +# Directories where sample/dummy credentials are allowed (mirror of +# .config/CredScanSuppressions.json). Matched against repo-relative, '/'-normalized paths. +$excludedPrefixes = @('tests/', 'benchmarks/', 'eng/', 'OneBranchPipelines/', '.gdn/', '.git/') + +$sarifFiles = Get-ChildItem -Path $SearchRoot -Recurse -Filter '*.sarif' -ErrorAction SilentlyContinue +if (-not $sarifFiles) { + Write-Host "No SARIF output found under '$SearchRoot' - secret scanner did not produce results." + exit 0 +} + +$srcNorm = ($SourcesDir -replace '\\', '/').TrimEnd('/') +$violations = New-Object System.Collections.Generic.List[string] + +foreach ($file in $sarifFiles) { + try { + $sarif = Get-Content -LiteralPath $file.FullName -Raw | ConvertFrom-Json + } catch { + Write-Host "##[warning]Could not parse SARIF '$($file.FullName)': $_" + continue + } + + foreach ($run in @($sarif.runs)) { + foreach ($result in @($run.results)) { + if ($result.ruleId -notmatch '^SEC101') { continue } + + foreach ($loc in @($result.locations)) { + $uri = $loc.physicalLocation.artifactLocation.uri + if (-not $uri) { continue } + + $rel = $uri -replace '^file:///', '' -replace '\\', '/' + if ($srcNorm) { $rel = $rel -replace ('^' + [regex]::Escape($srcNorm) + '/'), '' } + $rel = $rel.TrimStart('/') + + $skip = $false + foreach ($prefix in $excludedPrefixes) { + if ($rel.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { $skip = $true; break } + } + if ($skip) { continue } + + $line = $loc.physicalLocation.region.startLine + $msg = $result.message.text + $violations.Add("${rel}:${line} $($result.ruleId) $msg") + } + } + } +} + +if ($violations.Count -gt 0) { + Write-Host "##[error]Secret scan found $($violations.Count) credential leak(s) in shipping source (outside allowed sample paths):" + foreach ($v in $violations) { Write-Host "##[error] $v" } + Write-Host "" + Write-Host "If a finding is an intentional sample credential, place it under an allowed path" + Write-Host "(tests/, benchmarks/, eng/, OneBranchPipelines/) or add a Guardian baseline entry." + exit 1 +} + +Write-Host "Secret scan gate passed: no SEC101 findings in shipping source." +exit 0 From eba34e02d4bee9edd1bd953392f07a8b4de86c58 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma <223556219+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:24:03 +0530 Subject: [PATCH 4/6] CHORE: fix secret-scan gate to read the scanner's hidden .gdn SARIF the gate passed even though the scanner flagged the root probe: Get-ChildItem -Recurse skips the hidden .gdn results dir without -Force, so it matched an unrelated empty SARIF. target /.gdn/.r directly with -Force (fallback to a forced recursive search) and add diagnostics on which SARIF files and how many SEC101 results are seen. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/scripts/secretscan-gate.ps1 | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/eng/pipelines/scripts/secretscan-gate.ps1 b/eng/pipelines/scripts/secretscan-gate.ps1 index d6569622..33938173 100644 --- a/eng/pipelines/scripts/secretscan-gate.ps1 +++ b/eng/pipelines/scripts/secretscan-gate.ps1 @@ -22,12 +22,25 @@ $ErrorActionPreference = 'Stop' # .config/CredScanSuppressions.json). Matched against repo-relative, '/'-normalized paths. $excludedPrefixes = @('tests/', 'benchmarks/', 'eng/', 'OneBranchPipelines/', '.gdn/', '.git/') -$sarifFiles = Get-ChildItem -Path $SearchRoot -Recurse -Filter '*.sarif' -ErrorAction SilentlyContinue +# The scanner writes its SARIF under /.gdn/.r/**. That directory is +# hidden, so a plain -Recurse would skip it (and could match an unrelated empty +# SARIF elsewhere) - search the known dir first, with -Force, then fall back. +$sarifFiles = @() +$gdnResults = Join-Path $SearchRoot '.gdn/.r' +if (Test-Path $gdnResults) { + $sarifFiles = @(Get-ChildItem -Path $gdnResults -Recurse -Filter '*.sarif' -Force -ErrorAction SilentlyContinue) +} +if (-not $sarifFiles) { + $sarifFiles = @(Get-ChildItem -Path $SearchRoot -Recurse -Filter '*.sarif' -Force -ErrorAction SilentlyContinue) +} if (-not $sarifFiles) { Write-Host "No SARIF output found under '$SearchRoot' - secret scanner did not produce results." exit 0 } +Write-Host "Found $($sarifFiles.Count) SARIF file(s):" +$sarifFiles | ForEach-Object { Write-Host " $($_.FullName)" } + $srcNorm = ($SourcesDir -replace '\\', '/').TrimEnd('/') $violations = New-Object System.Collections.Generic.List[string] @@ -40,6 +53,8 @@ foreach ($file in $sarifFiles) { } foreach ($run in @($sarif.runs)) { + $sec = @(@($run.results) | Where-Object { $_.ruleId -match '^SEC101' }) + Write-Host " $($file.Name): $(@($run.results).Count) result(s), $($sec.Count) SEC101" foreach ($result in @($run.results)) { if ($result.ruleId -notmatch '^SEC101') { continue } From f623245a514bbca04a353b782d1c440f58c1dcdd Mon Sep 17 00:00:00 2001 From: Gaurav Sharma <223556219+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:36:41 +0530 Subject: [PATCH 5/6] CHORE: resolve SPMI rule id across SARIF variants in secret-scan gate the gate read the right SARIF but saw 0 SEC101: SPMI stores the rule id via ruleIndex into tool.driver.rules[], not always in result.ruleId. resolve the id from result.ruleId, result.rule.id, or the rules table, match SEC101 anywhere, and log each result's ids for diagnosis. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/scripts/secretscan-gate.ps1 | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/eng/pipelines/scripts/secretscan-gate.ps1 b/eng/pipelines/scripts/secretscan-gate.ps1 index 33938173..f85ac344 100644 --- a/eng/pipelines/scripts/secretscan-gate.ps1 +++ b/eng/pipelines/scripts/secretscan-gate.ps1 @@ -53,10 +53,19 @@ foreach ($file in $sarifFiles) { } foreach ($run in @($sarif.runs)) { - $sec = @(@($run.results) | Where-Object { $_.ruleId -match '^SEC101' }) - Write-Host " $($file.Name): $(@($run.results).Count) result(s), $($sec.Count) SEC101" + $rules = @($run.tool.driver.rules) + Write-Host " $($file.Name): $(@($run.results).Count) result(s), $($rules.Count) rule(s) in driver" foreach ($result in @($run.results)) { - if ($result.ruleId -notmatch '^SEC101') { continue } + # Resolve the rule id across SARIF variants: result.ruleId, result.rule.id, + # or result.ruleIndex -> tool.driver.rules[index].id + $rid = $result.ruleId + if (-not $rid -and $result.rule) { $rid = $result.rule.id } + if (($rid -notmatch 'SEC101') -and ($null -ne $result.ruleIndex) -and $rules.Count -gt 0) { + $idx = [int]$result.ruleIndex + if ($idx -ge 0 -and $idx -lt $rules.Count) { $rid = $rules[$idx].id } + } + Write-Host " result: ruleId='$($result.ruleId)' ruleIndex='$($result.ruleIndex)' resolved='$rid'" + if ($rid -notmatch 'SEC101') { continue } foreach ($loc in @($result.locations)) { $uri = $loc.physicalLocation.artifactLocation.uri @@ -74,7 +83,7 @@ foreach ($file in $sarifFiles) { $line = $loc.physicalLocation.region.startLine $msg = $result.message.text - $violations.Add("${rel}:${line} $($result.ruleId) $msg") + $violations.Add("${rel}:${line} ${rid} $msg") } } } From 53b07a1bc02f95d35e9987f0d0cb0ff1d4b2377b Mon Sep 17 00:00:00 2001 From: Gaurav Sharma <223556219+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:41:10 +0530 Subject: [PATCH 6/6] CHORE: remove secret-scan probe and trim gate diagnostics detection proven (build failed on the root probe with SEC101/037 while ignoring the sample creds under tests/). remove the probe so the gate now passes on a clean tree, and drop the per-result debug line. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- _secretscan_probe.txt | 9 --------- eng/pipelines/scripts/secretscan-gate.ps1 | 1 - 2 files changed, 10 deletions(-) delete mode 100644 _secretscan_probe.txt diff --git a/_secretscan_probe.txt b/_secretscan_probe.txt deleted file mode 100644 index 62ac2db6..00000000 --- a/_secretscan_probe.txt +++ /dev/null @@ -1,9 +0,0 @@ -SECRET-SCAN PROBE — NOT A REAL CREDENTIAL. DELETE BEFORE MERGE. - -This file exists only to verify that the shift-left secret-scanning stage in the -PR-validation pipeline catches the SEC101/037 "SqlLegacyCredentials" pattern -(realistic Azure SQL hostname + UID + PWD) at PR time, pre-sync. - -The value below is fabricated and points at a non-existent host: - -Server=tcp:contoso-probe-doesnotexist.database.windows.net,1433;Database=probe;UID=probeuser;PWD=Fake_NotARealSecret_2026;Encrypt=yes;TrustServerCertificate=no; diff --git a/eng/pipelines/scripts/secretscan-gate.ps1 b/eng/pipelines/scripts/secretscan-gate.ps1 index f85ac344..88154932 100644 --- a/eng/pipelines/scripts/secretscan-gate.ps1 +++ b/eng/pipelines/scripts/secretscan-gate.ps1 @@ -64,7 +64,6 @@ foreach ($file in $sarifFiles) { $idx = [int]$result.ruleIndex if ($idx -ge 0 -and $idx -lt $rules.Count) { $rid = $rules[$idx].id } } - Write-Host " result: ruleId='$($result.ruleId)' ruleIndex='$($result.ruleIndex)' resolved='$rid'" if ($rid -notmatch 'SEC101') { continue } foreach ($loc in @($result.locations)) {