From c66069fe4b95a7b852cb938da1105e91ee760b87 Mon Sep 17 00:00:00 2001 From: Mick Grove Date: Mon, 22 Dec 2025 09:45:58 -0800 Subject: [PATCH] - Map SARIF result levels from rule confidence - Added tag selection support to the bash and PowerShell install scripts. --- .github/workflows/ci.yml | 35 +++++++++++++++-- CHANGELOG.md | 2 + README.md | 14 +++++++ scripts/install-kingfisher.ps1 | 29 +++++++++++---- scripts/install-kingfisher.sh | 53 ++++++++++++++++++++++---- src/reporter/sarif_format.rs | 68 +++++++++++++++++++++++++++++++++- 6 files changed, 181 insertions(+), 20 deletions(-) mode change 100644 => 100755 scripts/install-kingfisher.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1080ff4..61442e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,16 +83,43 @@ jobs: vcpkg- # Ensure downloads dir exists and seed PCRE 8.45 zip from a working mirror - - name: Pre-seed PCRE 8.45 for vcpkg (bypass SourceForge redirect) + - name: Pre-seed PCRE 8.45 for vcpkg shell: pwsh run: | New-Item -ItemType Directory -Force -Path "$env:VCPKG_DOWNLOADS" | Out-Null $dst = Join-Path $env:VCPKG_DOWNLOADS "pcre-8.45.zip" + if (-not (Test-Path $dst)) { - Invoke-WebRequest ` - -Uri "https://versaweb.dl.sourceforge.net/project/pcre/pcre/8.45/pcre-8.45.zip" ` - -OutFile $dst -UseBasicParsing + $sf = "https://sourceforge.net/projects/pcre/files/pcre/8.45/pcre-8.45.zip/download" + + # Resolve to the final mirror URL (follow redirects without downloading the whole file) + $handler = New-Object System.Net.Http.HttpClientHandler + $handler.AllowAutoRedirect = $true + $client = New-Object System.Net.Http.HttpClient($handler) + + try { + $req = New-Object System.Net.Http.HttpRequestMessage([System.Net.Http.HttpMethod]::Head, $sf) + $resp = $client.SendAsync($req).GetAwaiter().GetResult() + + # Some mirrors don’t like HEAD; fall back to GET headers only. + if (-not $resp.IsSuccessStatusCode) { + $req.Dispose() + $req = New-Object System.Net.Http.HttpRequestMessage([System.Net.Http.HttpMethod]::Get, $sf) + $resp = $client.SendAsync($req, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult() + } + + $finalUrl = $resp.RequestMessage.RequestUri.AbsoluteUri + Write-Host "Resolved SourceForge URL to: $finalUrl" + + # Download the actual file + Invoke-WebRequest -Uri $finalUrl -OutFile $dst + } + finally { + $client.Dispose() + $handler.Dispose() + } } + Get-ChildItem $env:VCPKG_DOWNLOADS - uses: swatinem/rust-cache@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d7a805..27d0b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. - Fixed deduplication for dependency-provider rules so dependent validations run per blob - Updated Artifactory rule entropy and added new artifactory rule - Aliased "kingfisher self-update" as "kingfisher update" +- Map SARIF result levels from rule confidence +- Added tag selection support to the bash and PowerShell install scripts. ## [v1.71.0] - Improved Report Viewer layout diff --git a/README.md b/README.md index 7ba7217..35dfdd4 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,14 @@ curl --silent --location \ bash -s -- /opt/kingfisher ``` +To install a specific tag: + +```bash +curl --silent --location \ + https://raw.githubusercontent.com/mongodb/kingfisher/main/scripts/install-kingfisher.sh | \ + bash -s -- --tag v1.71.0 +``` + ### Windows @@ -230,6 +238,12 @@ You can provide a custom destination using the `-InstallDir` parameter: ```powershell ./install-kingfisher.ps1 -InstallDir 'C:\Tools\Kingfisher' ``` + +To install a specific tag: + +```powershell +./install-kingfisher.ps1 -Tag v1.71.0 +``` diff --git a/scripts/install-kingfisher.ps1 b/scripts/install-kingfisher.ps1 old mode 100644 new mode 100755 index 5e2405c..fde1cf2 --- a/scripts/install-kingfisher.ps1 +++ b/scripts/install-kingfisher.ps1 @@ -1,28 +1,35 @@ <# .SYNOPSIS - Download and install the latest Kingfisher release for Windows. + Download and install a Kingfisher release for Windows. .DESCRIPTION - Fetches the most recent GitHub release for mongodb/kingfisher, downloads the - Windows x64 archive, and extracts kingfisher.exe to the destination folder. - By default the script installs into "$env:USERPROFILE\bin". + Fetches a GitHub release for mongodb/kingfisher, downloads the Windows x64 + archive, and extracts kingfisher.exe to the destination folder. By default + the script installs into "$env:USERPROFILE\bin". .PARAMETER InstallDir Optional destination directory for the kingfisher.exe binary. +.PARAMETER Tag + Optional GitHub release tag (e.g., v1.71.0). Defaults to the latest release. + .EXAMPLE ./install-kingfisher.ps1 .EXAMPLE ./install-kingfisher.ps1 -InstallDir "C:\\Tools" + +.EXAMPLE + ./install-kingfisher.ps1 -Tag v1.71.0 #> param( [Parameter(Position = 0)] - [string]$InstallDir = (Join-Path $env:USERPROFILE 'bin') + [string]$InstallDir = (Join-Path $env:USERPROFILE 'bin'), + + [string]$Tag ) $repo = 'mongodb/kingfisher' -$apiUrl = "https://api.github.com/repos/$repo/releases/latest" $assetName = 'kingfisher-windows-x64.zip' if (-not (Get-Command Invoke-WebRequest -ErrorAction SilentlyContinue)) { @@ -33,7 +40,13 @@ if (-not (Get-Command Expand-Archive -ErrorAction SilentlyContinue)) { throw 'Expand-Archive is required to extract the release archive. Install the PowerShell archive module.' } -Write-Host "Fetching latest release metadata for $repo…" +if ($Tag) { + $apiUrl = "https://api.github.com/repos/$repo/releases/tags/$Tag" + Write-Host "Fetching release metadata for $repo tag $Tag…" +} else { + $apiUrl = "https://api.github.com/repos/$repo/releases/latest" + Write-Host "Fetching latest release metadata for $repo…" +} try { $response = Invoke-WebRequest -Uri $apiUrl -UseBasicParsing $release = $response.Content | ConvertFrom-Json @@ -44,7 +57,7 @@ try { $releaseTag = $release.tag_name $asset = $release.assets | Where-Object { $_.name -eq $assetName } if (-not $asset) { - throw "Could not find asset '$assetName' in the latest release." + throw "Could not find asset '$assetName' in the release metadata." } $tempDir = New-Item -ItemType Directory -Path ([System.IO.Path]::GetTempPath()) -Name ([System.Guid]::NewGuid().ToString()) diff --git a/scripts/install-kingfisher.sh b/scripts/install-kingfisher.sh index 4bb2386..517b5f0 100755 --- a/scripts/install-kingfisher.sh +++ b/scripts/install-kingfisher.sh @@ -3,16 +3,19 @@ set -euo pipefail REPO="mongodb/kingfisher" DEFAULT_INSTALL_DIR="$HOME/.local/bin" -LATEST_DL_BASE="https://github.com/${REPO}/releases/latest/download" +TAG="" usage() { cat <<'USAGE' -Usage: install-kingfisher.sh [INSTALL_DIR] +Usage: install-kingfisher.sh [OPTIONS] [INSTALL_DIR] -Downloads the latest Kingfisher release for Linux or macOS and installs the -binary into INSTALL_DIR (default: ~/.local/bin). +Downloads a Kingfisher release for Linux or macOS and installs the binary into +INSTALL_DIR (default: ~/.local/bin). Requirements: curl, tar + +Options: + -t, --tag TAG Install a specific release tag (e.g., v1.71.0). USAGE } @@ -21,7 +24,35 @@ if [[ "${1-}" == "-h" || "${1-}" == "--help" ]]; then exit 0 fi -INSTALL_DIR="${1:-$DEFAULT_INSTALL_DIR}" +INSTALL_DIR="$DEFAULT_INSTALL_DIR" + +while [[ $# -gt 0 ]]; do + case "$1" in + -t|--tag) + if [[ -z "${2-}" ]]; then + echo "Error: --tag requires a value." >&2 + usage + exit 1 + fi + TAG="$2" + shift 2 + ;; + -*) + echo "Error: Unknown option '$1'." >&2 + usage + exit 1 + ;; + *) + if [[ "$INSTALL_DIR" != "$DEFAULT_INSTALL_DIR" ]]; then + echo "Error: INSTALL_DIR specified multiple times." >&2 + usage + exit 1 + fi + INSTALL_DIR="$1" + shift + ;; + esac +done # deps command -v curl >/dev/null 2>&1 || { echo "Error: curl is required." >&2; exit 1; } @@ -45,7 +76,15 @@ esac asset_name="kingfisher-${platform}-${arch_suffix}.tgz" : "${asset_name:?internal error: asset_name not set}" # guard for set -u -download_url="${LATEST_DL_BASE}/${asset_name}" +if [[ -n "$TAG" ]]; then + dl_base="https://github.com/${REPO}/releases/download/${TAG}" + release_label="release tag ${TAG}" +else + dl_base="https://github.com/${REPO}/releases/latest/download" + release_label="latest release" +fi + +download_url="${dl_base}/${asset_name}" tmpdir="$(mktemp -d)" cleanup() { rm -rf "$tmpdir"; } @@ -53,7 +92,7 @@ trap cleanup EXIT archive_path="$tmpdir/$asset_name" -echo "Downloading latest: ${asset_name} …" +echo "Downloading ${release_label}: ${asset_name} …" # -f: fail on HTTP errors (e.g., 404 if asset missing) if ! curl -fLsS "${download_url}" -o "$archive_path"; then echo "Error: Failed to download ${download_url}" >&2 diff --git a/src/reporter/sarif_format.rs b/src/reporter/sarif_format.rs index 9f552ec..2eb7446 100644 --- a/src/reporter/sarif_format.rs +++ b/src/reporter/sarif_format.rs @@ -7,6 +7,15 @@ use super::*; use crate::defaults::get_builtin_rules; impl DetailsReporter { + fn sarif_level_for_confidence(confidence: &str) -> sarif::ResultLevel { + match confidence.to_ascii_lowercase().as_str() { + "low" => sarif::ResultLevel::Note, + "medium" => sarif::ResultLevel::Warning, + "high" => sarif::ResultLevel::Error, + _ => sarif::ResultLevel::Warning, + } + } + fn record_to_sarif_result(&self, record: &FindingReporterRecord) -> Result { let finding = &record.finding; let artifact_location = @@ -49,7 +58,7 @@ impl DetailsReporter { .message(message) .kind(sarif::ResultKind::Review.to_string()) .locations(vec![location]) - .level(sarif::ResultLevel::Warning.to_string()) + .level(Self::sarif_level_for_confidence(&finding.confidence).to_string()) .partial_fingerprints([("fingerprint".to_string(), finding.fingerprint.clone())]) .build()?; Ok(result) @@ -132,3 +141,60 @@ impl DetailsReporter { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{findings_store::FindingsStore, reporter::styles::Styles}; + use std::sync::{Arc, Mutex}; + use tempfile::tempdir; + + fn test_reporter() -> DetailsReporter { + let tmp = tempdir().expect("tempdir"); + let store = FindingsStore::new(tmp.path().to_path_buf()); + DetailsReporter { + datastore: Arc::new(Mutex::new(store)), + styles: Styles::new(false), + only_valid: false, + } + } + + fn sample_record(confidence: &str) -> FindingReporterRecord { + FindingReporterRecord { + rule: RuleMetadata { name: "test-rule".to_string(), id: "rule-1".to_string() }, + finding: FindingRecordData { + snippet: "secret".to_string(), + fingerprint: "fingerprint".to_string(), + confidence: confidence.to_string(), + entropy: "0.0".to_string(), + validation: ValidationInfo { + status: "unknown".to_string(), + response: "n/a".to_string(), + }, + language: "Rust".to_string(), + line: 1, + column_start: 1, + column_end: 5, + path: "src/lib.rs".to_string(), + encoding: None, + git_metadata: None, + }, + } + } + + #[test] + fn sarif_level_maps_from_confidence() { + let reporter = test_reporter(); + let low = reporter.record_to_sarif_result(&sample_record("low")).unwrap(); + let medium = reporter.record_to_sarif_result(&sample_record("medium")).unwrap(); + let high = reporter.record_to_sarif_result(&sample_record("high")).unwrap(); + + let expected_low = sarif::ResultLevel::Note.to_string(); + let expected_medium = sarif::ResultLevel::Warning.to_string(); + let expected_high = sarif::ResultLevel::Error.to_string(); + + assert_eq!(low.level.as_deref(), Some(expected_low.as_str())); + assert_eq!(medium.level.as_deref(), Some(expected_medium.as_str())); + assert_eq!(high.level.as_deref(), Some(expected_high.as_str())); + } +}