- Map SARIF result levels from rule confidence

- Added tag selection support to the bash and PowerShell install scripts.
This commit is contained in:
Mick Grove 2025-12-22 09:45:58 -08:00
commit c66069fe4b
6 changed files with 181 additions and 20 deletions

View file

@ -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 dont 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

View file

@ -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

View file

@ -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
```
</details>
### 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
```
</details>

29
scripts/install-kingfisher.ps1 Normal file → Executable file
View file

@ -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())

View file

@ -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

View file

@ -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<sarif::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()));
}
}