forked from mirrors/kingfisher
added dark mode for finding + access map viewer
This commit is contained in:
parent
3a579dd6ca
commit
195f086afc
25 changed files with 284 additions and 193 deletions
|
|
@ -3,7 +3,7 @@
|
|||
description: Run Kingfisher in Docker against staged changes at the repository root. No local install required.
|
||||
entry: ghcr.io/kingfisher-sec/kingfisher:latest
|
||||
language: docker
|
||||
args: ["scan", ".", "--staged", "--quiet", "--redact", "--only-valid", "--no-update-check"]
|
||||
args: ["scan", ".", "--staged", "--quiet", "--no-update-check"]
|
||||
pass_filenames: false
|
||||
stages: [commit]
|
||||
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
description: Scan staged changes with the locally installed Kingfisher binary.
|
||||
entry: kingfisher
|
||||
language: system
|
||||
args: ["scan", ".", "--staged", "--quiet", "--redact", "--only-valid", "--no-update-check"]
|
||||
args: ["scan", ".", "--staged", "--quiet", "--no-update-check"]
|
||||
pass_filenames: false
|
||||
types: [file]
|
||||
stages: [commit]
|
||||
|
|
@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
|
|||
- Added `--staged` argument to support new `pre-commit` behavior and added integration coverage to ensure validated secrets block commits when used as pre-commit hook
|
||||
- Added new rules for AWS Bedrock, Voyage.ai, Posthog, Atlassian
|
||||
- Added an embedded web-based report and access-map viewer via `kingfisher view` subcommand that can load JSON or JSONL reports passed on the CLI (or upload them in the browser)
|
||||
- Added a check for network connectivity via `online` crate before attempting validation.
|
||||
|
||||
## [v1.69.0]
|
||||
- Reduced per-match memory usage by compacting stored source locations and interning repeated capture names.
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ Install a **per-repository** hook from the root of the target repo:
|
|||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
|
||||
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/mongodb/kingfisher/main/scripts/install-kingfisher-pre-commit.ps1' -OutFile install-kingfisher-pre-commit.ps1
|
||||
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/mongodb/kingfisher/development/scripts/install-kingfisher-pre-commit.ps1' -OutFile install-kingfisher-pre-commit.ps1
|
||||
./install-kingfisher-pre-commit.ps1
|
||||
```
|
||||
|
||||
|
|
@ -298,7 +298,7 @@ Uninstall the **global** hook:
|
|||
```
|
||||
|
||||
> The installer automatically runs any existing `pre-commit` hook first, then
|
||||
> executes `kingfisher scan . --staged --quiet --redact --only-valid --no-update-check`
|
||||
> executes `kingfisher scan . --staged --quiet --no-update-check`
|
||||
> against the staged diff (anchored to `HEAD` when no commits exist yet).
|
||||
|
||||
</details>
|
||||
|
|
@ -325,7 +325,7 @@ Then install the hook via `pre-commit install`. Every hook now drives Kingfisher
|
|||
directly with the built-in `--staged` flag:
|
||||
|
||||
```bash
|
||||
kingfisher scan . --staged --quiet --redact --only-valid --no-update-check
|
||||
kingfisher scan . --staged --quiet --no-update-check
|
||||
```
|
||||
|
||||
When `--staged` is set, Kingfisher snapshots the staged index into a temporary
|
||||
|
|
@ -551,6 +551,7 @@ kingfisher scan /path/to/repo --format sarif --output findings.sarif
|
|||
|
||||
- Add `--access-map` to enrich JSON, JSONL, BSON, pretty, and SARIF reports with an `access_map` array containing providers, accounts/projects, resources, and the permissions available for each resource (grouped when identical).
|
||||
- If you validated cloud credentials without `--access-map`, Kingfisher will remind you on stderr to rerun with the flag so the access map appears in the output.
|
||||
- Use the access map functionality only when you are authorized to inspect the target account, as Kingfisher will issue additional network requests to determine what access the secret grants.
|
||||
- Run `kingfisher view ./kingfisher.json` to explore a report locally in a local web UI
|
||||
|
||||
### View access-map reports locally
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ rules:
|
|||
\b
|
||||
(?:AWS|AMAZON|AMZN|A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)
|
||||
(?:.|[\n\r]){0,64}?
|
||||
\b
|
||||
[^A-Za-z0-9_+!@\#$%^&*()\]./]
|
||||
([A-Za-z0-9/+]{40})
|
||||
\b
|
||||
[^A-Za-z0-9_+!@\#$%^&*()\]./]
|
||||
|
|
||||
\b(?:AWS|AMAZON|AMZN|A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)
|
||||
(?:.|[\n\r]){0,96}?
|
||||
|
|
@ -43,7 +43,7 @@ rules:
|
|||
\b
|
||||
)
|
||||
pattern_requirements:
|
||||
min_digits: 2
|
||||
min_digits: 3
|
||||
ignore_if_contains:
|
||||
- "EXAMPLE"
|
||||
- "TEST"
|
||||
|
|
|
|||
|
|
@ -1,16 +1,40 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Kingfisher Access Map Viewer</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--brand: #0e7c56;
|
||||
--brand-dark: #0a5d40;
|
||||
--brand-soft: #123025;
|
||||
--bg: #0b1220;
|
||||
--surface: #111827;
|
||||
--surface-muted: #0f172a;
|
||||
--text-main: #e5e7eb;
|
||||
--text-muted: #9ca3af;
|
||||
--border: #1f2937;
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.15);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.25), 0 2px 4px -2px rgb(0 0 0 / 0.2);
|
||||
--critical: #fca5a5;
|
||||
--success: #34d399;
|
||||
--radius: 8px;
|
||||
--hover: #1f2937;
|
||||
--table-header: #0f172a;
|
||||
--code-bg: #0f172a;
|
||||
--code-border: #1f2937;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
--brand: #0e7c56;
|
||||
--brand-dark: #07402c;
|
||||
--brand-soft: #e6f4ed;
|
||||
--bg: #f3f4f6;
|
||||
--surface: #ffffff;
|
||||
--surface-muted: #f9fafb;
|
||||
--text-main: #111827;
|
||||
--text-muted: #6b7280;
|
||||
--border: #e5e7eb;
|
||||
|
|
@ -18,7 +42,10 @@
|
|||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--critical: #dc2626;
|
||||
--success: #059669;
|
||||
--radius: 8px;
|
||||
--hover: #e5e7eb;
|
||||
--table-header: #f9fafb;
|
||||
--code-bg: #0f172a;
|
||||
--code-border: #1f2937;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
|
@ -103,7 +130,7 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.panel__title h3 { margin: 0; font-size: 16px; font-weight: 600; }
|
||||
|
|
@ -113,7 +140,7 @@
|
|||
.upload-area {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
background: #f9fafb;
|
||||
background: var(--surface-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
|
@ -169,7 +196,7 @@
|
|||
|
||||
.am-sidebar {
|
||||
border-right: 1px solid var(--border);
|
||||
background: #f9fafb;
|
||||
background: var(--surface-muted);
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
|
@ -177,13 +204,13 @@
|
|||
.am-main {
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.am-search {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #f9fafb;
|
||||
background: var(--surface-muted);
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
|
@ -196,6 +223,8 @@
|
|||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
background: var(--surface);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.am-search input:focus {
|
||||
|
|
@ -237,7 +266,7 @@
|
|||
}
|
||||
|
||||
.node-content:hover {
|
||||
background: #e5e7eb;
|
||||
background: var(--hover);
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
|
|
@ -286,7 +315,7 @@
|
|||
border-radius: 8px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--bg);
|
||||
background: var(--surface-muted);
|
||||
border: 1px solid var(--border);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
|
@ -341,7 +370,7 @@
|
|||
.table th {
|
||||
text-align: left;
|
||||
padding: 12px 16px;
|
||||
background: #f9fafb;
|
||||
background: var(--table-header);
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
|
|
@ -352,7 +381,7 @@
|
|||
vertical-align: top;
|
||||
}
|
||||
.table tr:last-child td { border-bottom: none; }
|
||||
.table tr:hover td { background: #f9fafb; cursor: pointer; }
|
||||
.table tr:hover td { background: var(--surface-muted); cursor: pointer; }
|
||||
|
||||
.sortable {
|
||||
cursor: pointer;
|
||||
|
|
@ -375,11 +404,11 @@
|
|||
}
|
||||
.status-badge.active { background: #ecfdf5; color: #15803d; }
|
||||
.status-badge.inactive { background: #fef2f2; color: #b91c1c; }
|
||||
.status-badge.unknown { background: #f3f4f6; color: #6b7280; }
|
||||
.status-badge.unknown { background: var(--surface-muted); color: var(--text-muted); }
|
||||
|
||||
/* Finding detail snippet */
|
||||
.snippet-box {
|
||||
background: #0f172a;
|
||||
background: var(--code-bg);
|
||||
color: #e5e7eb;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
|
|
@ -387,7 +416,7 @@
|
|||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
border: 1px solid #1f2937;
|
||||
border: 1px solid var(--code-border);
|
||||
}
|
||||
|
||||
#fd-validation-box {
|
||||
|
|
@ -416,7 +445,7 @@
|
|||
.path-mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
background: #f3f4f6;
|
||||
background: var(--surface-muted);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
|
|
@ -444,6 +473,8 @@
|
|||
border-radius: 6px;
|
||||
width: 220px;
|
||||
font-size: 13px;
|
||||
background: var(--surface);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.rows-label {
|
||||
|
|
@ -455,8 +486,9 @@
|
|||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
font-size: 13px;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.pager {
|
||||
|
|
@ -472,11 +504,12 @@
|
|||
height: 28px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.pager-btn:disabled {
|
||||
|
|
@ -486,7 +519,7 @@
|
|||
|
||||
/* Buttons & loader */
|
||||
.btn {
|
||||
background: #ffffff;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
|
|
@ -496,8 +529,8 @@
|
|||
transition: background 0.1s, border-color 0.1s;
|
||||
}
|
||||
.btn:hover {
|
||||
background: #f3f4f6;
|
||||
border-color: #cbd5e1;
|
||||
background: var(--surface-muted);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.hidden { display: none !important; }
|
||||
|
|
@ -516,7 +549,7 @@
|
|||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 999px;
|
||||
border: 3px solid #e5e7eb;
|
||||
border: 3px solid var(--border);
|
||||
border-top-color: #0e7c56;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
|
@ -525,7 +558,7 @@
|
|||
width: 220px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: #e5e7eb;
|
||||
background: var(--border);
|
||||
overflow: hidden;
|
||||
margin: 12px auto 0;
|
||||
}
|
||||
|
|
@ -548,7 +581,7 @@
|
|||
<body>
|
||||
|
||||
<div id="loader" class="loading-overlay hidden">
|
||||
<div style="background:#ffffff; padding:18px 22px; border-radius:10px; border:1px solid var(--border); box-shadow:var(--shadow-md); text-align:center; min-width:260px;">
|
||||
<div style="background:var(--surface); padding:18px 22px; border-radius:10px; border:1px solid var(--border); box-shadow:var(--shadow-md); text-align:center; min-width:260px; color:var(--text-main);">
|
||||
<div class="spinner" style="margin:0 auto 12px;"></div>
|
||||
<div class="progress-track"><div class="progress-inner"></div></div>
|
||||
<div id="loader-text" style="margin-top:10px; font-size:13px; color:var(--text-muted);">
|
||||
|
|
@ -565,7 +598,8 @@
|
|||
<span class="hero__subtitle">Access Map & Findings</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:10px;">
|
||||
<div style="display:flex; gap:10px; align-items:center;">
|
||||
<button class="btn" id="theme-toggle" type="button">Light Mode</button>
|
||||
<button class="btn" id="reset-btn">Load New Report</button>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -811,6 +845,19 @@
|
|||
const amContainer = document.getElementById("am-container");
|
||||
const amToggle = document.getElementById("am-toggle");
|
||||
const copyAccessMapButton = document.getElementById("copy-access-map");
|
||||
const themeToggle = document.getElementById("theme-toggle");
|
||||
|
||||
const THEME_KEY = "access-map-viewer-theme";
|
||||
|
||||
function setTheme(theme) {
|
||||
const normalized = theme === "light" ? "light" : "dark";
|
||||
document.documentElement.setAttribute("data-theme", normalized);
|
||||
if (themeToggle) {
|
||||
themeToggle.textContent = normalized === "dark" ? "Light Mode" : "Dark Mode";
|
||||
}
|
||||
}
|
||||
|
||||
setTheme(localStorage.getItem(THEME_KEY));
|
||||
|
||||
dropZone.addEventListener("click", () => fileInput.click());
|
||||
fileInput.addEventListener("change", (e) => {
|
||||
|
|
@ -893,6 +940,14 @@
|
|||
downloadJsonBtn.addEventListener("click", downloadFindingsJson);
|
||||
downloadCsvBtn.addEventListener("click", downloadFindingsCsv);
|
||||
copyAccessMapButton.addEventListener("click", copyFilteredAccessMap);
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener("click", () => {
|
||||
const current = document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark";
|
||||
const next = current === "dark" ? "light" : "dark";
|
||||
localStorage.setItem(THEME_KEY, next);
|
||||
setTheme(next);
|
||||
});
|
||||
}
|
||||
|
||||
loadCliReport();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<#!
|
||||
.SYNOPSIS
|
||||
Install or remove Kingfisher Git pre-commit hooks (local or global).
|
||||
Install or remove Kingfisher Git pre-commit hooks (POSIX-safe).
|
||||
|
||||
.DESCRIPTION
|
||||
Supports repo installs, global installs (via core.hooksPath), and
|
||||
custom hook directories. Preserves existing hooks safely and provides
|
||||
uninstall behavior mirroring the Bash installer.
|
||||
Writes POSIX-compliant shell hooks that work on macOS, Linux,
|
||||
and Windows (Git for Windows / sh).
|
||||
Safely bootstraps ~/.git/hooks if needed.
|
||||
|
||||
.PARAMETER Global
|
||||
Install into the global Git hooks directory.
|
||||
|
||||
.PARAMETER HooksPath
|
||||
Manually override the hooks directory.
|
||||
Manually override the hooks directory (repo only).
|
||||
|
||||
.PARAMETER Uninstall
|
||||
Remove the Kingfisher hook and restore a legacy hook if present.
|
||||
|
|
@ -24,135 +24,152 @@ param(
|
|||
[switch]$Global
|
||||
)
|
||||
|
||||
function Ensure-InRepo {
|
||||
if (-not $Global -and -not (git rev-parse --is-inside-work-tree 2>$null)) {
|
||||
throw "This installer must be run inside a Git repository unless --Global is specified."
|
||||
function Ensure-GlobalHooksPath {
|
||||
$existing = git config --global --get core.hooksPath 2>$null
|
||||
if ($existing) {
|
||||
return $existing
|
||||
}
|
||||
|
||||
# Git expands ~, PowerShell should not
|
||||
$gitHooksPath = "~/.git/hooks"
|
||||
$fsHooksPath = Join-Path $HOME ".git\hooks"
|
||||
|
||||
if (-not (Test-Path $fsHooksPath)) {
|
||||
New-Item -ItemType Directory -Force -Path $fsHooksPath | Out-Null
|
||||
}
|
||||
|
||||
git config --global core.hooksPath $gitHooksPath
|
||||
Write-Host "Configured global Git hooks at $gitHooksPath"
|
||||
|
||||
return $gitHooksPath
|
||||
}
|
||||
|
||||
function Resolve-HooksPath {
|
||||
param([string]$Override, [switch]$Global)
|
||||
|
||||
# Explicit override wins
|
||||
if ($Override) {
|
||||
return (Resolve-Path $Override).Path
|
||||
}
|
||||
|
||||
# Global mode
|
||||
if ($Global) {
|
||||
$p = git config --global core.hooksPath 2>$null
|
||||
if (-not $p) {
|
||||
# Default global hooks directory
|
||||
$p = Join-Path $HOME ".git-hooks"
|
||||
git config --global core.hooksPath $p
|
||||
Write-Host "Configured global Git hooks at $p"
|
||||
$resolved = Resolve-Path -LiteralPath $Override -ErrorAction SilentlyContinue
|
||||
if ($resolved) {
|
||||
return $resolved.Path
|
||||
}
|
||||
return $p
|
||||
return [IO.Path]::GetFullPath($Override)
|
||||
}
|
||||
|
||||
if ($Global) {
|
||||
$configured = git config --global --get core.hooksPath 2>$null
|
||||
if ($configured) {
|
||||
return $configured
|
||||
}
|
||||
return Ensure-GlobalHooksPath
|
||||
}
|
||||
|
||||
# Repo mode
|
||||
$repoHooks = git rev-parse --git-path hooks 2>$null
|
||||
if (-not $repoHooks) { throw "Unable to resolve repository hooks path." }
|
||||
return $repoHooks.Trim()
|
||||
if ($repoHooks) {
|
||||
return $repoHooks.Trim()
|
||||
}
|
||||
|
||||
$fallback = Join-Path (Get-Location) ".git\hooks"
|
||||
Write-Host "Git repository not detected; using fallback hooks path $fallback"
|
||||
return $fallback
|
||||
}
|
||||
|
||||
function Uninstall-Kingfisher {
|
||||
param(
|
||||
[string]$PreCommit,
|
||||
[string]$Legacy,
|
||||
[string]$KFHook,
|
||||
[string]$Marker
|
||||
)
|
||||
param($PreCommit, $Legacy, $KFHook, $Marker)
|
||||
|
||||
# Only try to inspect hook if it exists
|
||||
if (Test-Path $PreCommit) {
|
||||
# Only restore if this is our wrapper
|
||||
if (Select-String -Quiet -SimpleMatch -Path $PreCommit -Pattern $Marker) {
|
||||
if (Test-Path $Legacy) {
|
||||
Move-Item -Force $Legacy $PreCommit
|
||||
& chmod +x $PreCommit 2>$null | Out-Null
|
||||
Write-Host "Restored previous pre-commit hook from $Legacy"
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
Remove-Item -Force $PreCommit
|
||||
Write-Host "Removed Kingfisher pre-commit wrapper."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Always clean up wrapper + legacy
|
||||
Remove-Item -Force -ErrorAction SilentlyContinue $KFHook, $Legacy
|
||||
Write-Host "Kingfisher pre-commit hook uninstalled."
|
||||
}
|
||||
|
||||
Ensure-InRepo
|
||||
|
||||
# Determine hooks directory safely
|
||||
# ------------------------------
|
||||
# Resolve hooks directory
|
||||
# ------------------------------
|
||||
$hooksDir = Resolve-HooksPath -Override $HooksPath -Global:$Global
|
||||
|
||||
if (-not (Test-Path $hooksDir)) {
|
||||
New-Item -ItemType Directory -Force -Path $hooksDir | Out-Null
|
||||
# Convert ~/.git/hooks to filesystem path if needed
|
||||
if ($hooksDir -eq "~/.git/hooks") {
|
||||
$fsHooksDir = Join-Path $HOME ".git\hooks"
|
||||
} else {
|
||||
$fsHooksDir = $hooksDir
|
||||
}
|
||||
|
||||
$preCommit = Join-Path $hooksDir "pre-commit"
|
||||
$legacy = Join-Path $hooksDir "pre-commit.legacy.kingfisher"
|
||||
$kfHook = Join-Path $hooksDir "kingfisher-pre-commit"
|
||||
if (-not (Test-Path $fsHooksDir)) {
|
||||
New-Item -ItemType Directory -Force -Path $fsHooksDir | Out-Null
|
||||
}
|
||||
|
||||
$preCommit = Join-Path $fsHooksDir "pre-commit"
|
||||
$legacy = Join-Path $fsHooksDir "pre-commit.legacy.kingfisher"
|
||||
$kfHook = Join-Path $fsHooksDir "kingfisher-pre-commit"
|
||||
$marker = "# Kingfisher pre-commit wrapper"
|
||||
|
||||
# ------------------------------
|
||||
# Uninstall
|
||||
# ------------------------------
|
||||
if ($Uninstall) {
|
||||
Uninstall-Kingfisher -PreCommit $preCommit -Legacy $legacy -KFHook $kfHook -Marker $marker
|
||||
Uninstall-Kingfisher $preCommit $legacy $kfHook $marker
|
||||
return
|
||||
}
|
||||
|
||||
# ---- Kingfisher hook ----
|
||||
$kfContent = @"
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# ------------------------------
|
||||
# Kingfisher hook (POSIX sh)
|
||||
# ------------------------------
|
||||
$kfContent = @'
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
if ! command -v kingfisher >/dev/null 2>&1; then
|
||||
echo "Kingfisher is not on PATH; skipping scan." >&2
|
||||
exit 0
|
||||
fi
|
||||
command -v kingfisher >/dev/null 2>&1 || exit 0
|
||||
|
||||
repo_root="\$(git rev-parse --show-toplevel)"
|
||||
cd "\$repo_root"
|
||||
repo_root="$(git rev-parse --show-toplevel)"
|
||||
cd "$repo_root"
|
||||
|
||||
kingfisher scan . --staged --quiet --redact --only-valid --no-update-check
|
||||
"@
|
||||
kingfisher scan . --staged --quiet --no-update-check
|
||||
'@
|
||||
|
||||
# ---- Wrapper ----
|
||||
# Note: No dirname logic here — absolute paths only
|
||||
$wrapper = @"
|
||||
#!/usr/bin/env bash
|
||||
$marker
|
||||
set -euo pipefail
|
||||
|
||||
legacy_hook="$legacy"
|
||||
kingfisher_hook="$kfHook"
|
||||
|
||||
if [[ -f "\$legacy_hook" && -x "\$legacy_hook" ]]; then
|
||||
"\$legacy_hook" "\$@"
|
||||
fi
|
||||
|
||||
"\$kingfisher_hook" "\$@"
|
||||
"@
|
||||
|
||||
# Write inner Kingfisher hook
|
||||
Set-Content -Path $kfHook -Value $kfContent -NoNewline
|
||||
Set-Content -Path $kfHook -Value $kfContent -NoNewline -Encoding ASCII
|
||||
& chmod +x $kfHook 2>$null | Out-Null
|
||||
|
||||
# Preserve existing hook ONLY if it exists
|
||||
# ------------------------------
|
||||
# Preserve existing hook
|
||||
# ------------------------------
|
||||
if (Test-Path $preCommit) {
|
||||
# And if it's not our wrapper
|
||||
if (-not (Select-String -Quiet -SimpleMatch -Path $preCommit -Pattern $marker)) {
|
||||
Move-Item -Force $preCommit $legacy
|
||||
& chmod +x $legacy 2>$null
|
||||
& chmod +x $legacy 2>$null | Out-Null
|
||||
Write-Host "Existing pre-commit hook preserved at $legacy"
|
||||
}
|
||||
}
|
||||
|
||||
# Write wrapper
|
||||
Set-Content -Path $preCommit -Value $wrapper -NoNewline
|
||||
# ------------------------------
|
||||
# Wrapper (POSIX-safe)
|
||||
# ------------------------------
|
||||
$wrapper = @'
|
||||
#!/usr/bin/env sh
|
||||
# Kingfisher pre-commit wrapper
|
||||
set -eu
|
||||
|
||||
hooks_dir="$(git rev-parse --git-path hooks)"
|
||||
legacy_hook="$hooks_dir/pre-commit.legacy.kingfisher"
|
||||
kf_hook="$hooks_dir/kingfisher-pre-commit"
|
||||
|
||||
if [ -f "$legacy_hook" ] && [ -x "$legacy_hook" ]; then
|
||||
"$legacy_hook" "$@"
|
||||
fi
|
||||
|
||||
"$kf_hook" "$@"
|
||||
'@
|
||||
|
||||
Set-Content -Path $preCommit -Value $wrapper -NoNewline -Encoding ASCII
|
||||
& chmod +x $preCommit 2>$null | Out-Null
|
||||
|
||||
Write-Host "Kingfisher pre-commit hook installed at $preCommit"
|
||||
|
|
|
|||
13
scripts/install-kingfisher-pre-commit.sh
Normal file → Executable file
13
scripts/install-kingfisher-pre-commit.sh
Normal file → Executable file
|
|
@ -58,12 +58,13 @@ if $GLOBAL; then
|
|||
fi
|
||||
HOOKS_PATH="$GLOBAL_PATH"
|
||||
else
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo "Error: must be run inside a Git repository unless using --global." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$HOOKS_PATH" ]]; then
|
||||
HOOKS_PATH="$(git rev-parse --git-path hooks)"
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
HOOKS_PATH="$(git rev-parse --git-path hooks)"
|
||||
else
|
||||
HOOKS_PATH="$PWD/.git/hooks"
|
||||
echo "Git repository not detected; using fallback hooks path $HOOKS_PATH"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
@ -113,7 +114,7 @@ fi
|
|||
repo_root="$(git rev-parse --show-toplevel)"
|
||||
cd "$repo_root"
|
||||
|
||||
kingfisher scan . --staged --quiet --redact --only-valid --no-update-check
|
||||
kingfisher scan . --staged --quiet --no-update-check
|
||||
EOF
|
||||
chmod +x "$KF_HOOK"
|
||||
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ use crate::util::get_writer_for_file_or_stdout;
|
|||
#[command(next_help_heading = "Output Options")]
|
||||
pub struct OutputArgs<Format: ValueEnum + Send + Sync + 'static> {
|
||||
/// Write output to the specified path (stdout if not given)
|
||||
#[arg(long, short, value_hint = ValueHint::FilePath)]
|
||||
#[arg(global = true, long, short, value_hint = ValueHint::FilePath)]
|
||||
pub output: Option<PathBuf>,
|
||||
|
||||
/// Output format (defaults to `pretty` if not specified)
|
||||
#[arg(long, short, default_value = "pretty")]
|
||||
#[arg(global = true, long, short, default_value = "pretty")]
|
||||
pub format: Format,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ fn default_scan_jobs() -> usize {
|
|||
#[derive(Args, Debug, Clone)]
|
||||
pub struct ScanArgs {
|
||||
/// Number of parallel scanning threads
|
||||
#[arg(long = "jobs", short = 'j', default_value_t = default_scan_jobs())]
|
||||
#[arg(global = true, long = "jobs", short = 'j', default_value_t = default_scan_jobs())]
|
||||
pub num_jobs: usize,
|
||||
|
||||
#[command(flatten)]
|
||||
|
|
@ -75,21 +75,22 @@ pub struct ScanArgs {
|
|||
pub content_filtering_args: ContentFilteringArgs,
|
||||
|
||||
/// Minimum confidence level for reporting findings
|
||||
#[arg(long, short = 'c', default_value = "medium")]
|
||||
#[arg(global = true, long, short = 'c', default_value = "medium")]
|
||||
pub confidence: ConfidenceLevel,
|
||||
|
||||
/// Disable secret validation
|
||||
#[arg(long, short = 'n', default_value_t = false)]
|
||||
#[arg(global = true, long, short = 'n', default_value_t = false)]
|
||||
pub no_validate: bool,
|
||||
|
||||
/// Map validated cloud credentials to their effective identities
|
||||
#[arg(long, default_value_t = false)]
|
||||
/// Map validated cloud credentials to their effective identities; use only when
|
||||
/// authorized for the target account because this triggers additional network
|
||||
/// requests to determine granted access
|
||||
#[arg(global = true, long, default_value_t = false)]
|
||||
pub access_map: bool,
|
||||
|
||||
/// Optional path to write a consolidated access-map HTML report
|
||||
#[arg(long, value_name = "PATH")]
|
||||
pub access_map_html: Option<PathBuf>,
|
||||
|
||||
// /// Optional path to write a consolidated access-map HTML report
|
||||
// #[arg(long, value_name = "PATH")]
|
||||
// pub access_map_html: Option<PathBuf>,
|
||||
/// Display only validated findings
|
||||
#[arg(long, default_value_t = false)]
|
||||
pub only_valid: bool,
|
||||
|
|
@ -103,58 +104,63 @@ pub struct ScanArgs {
|
|||
pub rule_stats: bool,
|
||||
|
||||
/// Display every occurrence of a finding
|
||||
#[arg(long, default_value_t = false)]
|
||||
#[arg(global = true, long, default_value_t = false)]
|
||||
pub no_dedup: bool,
|
||||
|
||||
/// Redact findings values using a secure hash
|
||||
#[arg(long, short = 'r', default_value_t = false)]
|
||||
#[arg(global = true, long, short = 'r', default_value_t = false)]
|
||||
pub redact: bool,
|
||||
|
||||
/// Skip decoding Base64 blobs before scanning
|
||||
#[arg(long, default_value_t = false)]
|
||||
#[arg(global = true, long, default_value_t = false)]
|
||||
pub no_base64: bool,
|
||||
|
||||
/// Timeout for Git repository scanning in seconds
|
||||
#[arg(long, default_value_t = 1800, value_name = "SECONDS")]
|
||||
#[arg(global = true, long, default_value_t = 1800, value_name = "SECONDS")]
|
||||
pub git_repo_timeout: u64,
|
||||
|
||||
#[command(flatten)]
|
||||
pub output_args: OutputArgs<ReportOutputFormat>,
|
||||
|
||||
/// Baseline file to filter known secrets
|
||||
#[arg(long, value_name = "FILE")]
|
||||
#[arg(global = true, long, value_name = "FILE")]
|
||||
pub baseline_file: Option<std::path::PathBuf>,
|
||||
|
||||
/// Create or update the baseline file with current findings
|
||||
#[arg(long, default_value_t = false)]
|
||||
#[arg(global = true, long, default_value_t = false)]
|
||||
pub manage_baseline: bool,
|
||||
|
||||
/// Regex patterns to allow-list secret matches (repeatable)
|
||||
#[arg(long = "skip-regex", value_name = "PATTERN")]
|
||||
#[arg(global = true, long = "skip-regex", value_name = "PATTERN")]
|
||||
pub skip_regex: Vec<String>,
|
||||
|
||||
/// Skipwords to allow-list secret matches (case-insensitive, repeatable)
|
||||
#[arg(long = "skip-word", value_name = "WORD")]
|
||||
#[arg(global = true, long = "skip-word", value_name = "WORD")]
|
||||
pub skip_word: Vec<String>,
|
||||
|
||||
/// AWS account IDs whose findings should skip live credential validation (repeatable)
|
||||
#[arg(long = "skip-aws-account", value_name = "ACCOUNT_ID", value_delimiter = ',')]
|
||||
#[arg(
|
||||
global = true,
|
||||
long = "skip-aws-account",
|
||||
value_name = "ACCOUNT_ID",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
pub skip_aws_account: Vec<String>,
|
||||
|
||||
/// File containing AWS account IDs to skip (one per line, `#` comments ignored)
|
||||
#[arg(long = "skip-aws-account-file", value_name = "FILE")]
|
||||
#[arg(global = true, long = "skip-aws-account-file", value_name = "FILE")]
|
||||
pub skip_aws_account_file: Option<PathBuf>,
|
||||
|
||||
/// Additional inline ignore directives to recognise (repeatable)
|
||||
#[arg(long = "ignore-comment", value_name = "DIRECTIVE")]
|
||||
#[arg(global = true, long = "ignore-comment", value_name = "DIRECTIVE")]
|
||||
pub extra_ignore_comments: Vec<String>,
|
||||
|
||||
/// Disable inline ignore directives entirely
|
||||
#[arg(long = "no-ignore", default_value_t = false)]
|
||||
#[arg(global = true, long = "no-ignore", default_value_t = false)]
|
||||
pub no_inline_ignore: bool,
|
||||
|
||||
/// Disable rule-level `ignore_if_contains` filtering for pattern requirements
|
||||
#[arg(long = "no-ignore-if-contains", default_value_t = false)]
|
||||
#[arg(global = true, long = "no-ignore-if-contains", default_value_t = false)]
|
||||
pub no_ignore_if_contains: bool,
|
||||
}
|
||||
|
||||
|
|
@ -432,10 +438,6 @@ impl ScanCommandArgs {
|
|||
self.scan_args.no_dedup = true;
|
||||
}
|
||||
|
||||
if self.scan_args.access_map_html.is_some() {
|
||||
self.scan_args.access_map = true;
|
||||
}
|
||||
|
||||
if self.scan_args.access_map && self.scan_args.no_validate {
|
||||
bail!("--access-map cannot be used with --no-validate");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -219,7 +219,11 @@ async fn async_main(args: CommandLineArgs) -> Result<()> {
|
|||
scan_args.input_specifier_args.path_inputs = vec![stdin_file.into()];
|
||||
}
|
||||
|
||||
let rules_db = Arc::new(load_and_record_rules(&scan_args, &datastore)?);
|
||||
let rules_db = Arc::new(load_and_record_rules(
|
||||
&scan_args,
|
||||
&datastore,
|
||||
global_args.use_progress(),
|
||||
)?);
|
||||
run_scan(
|
||||
&global_args,
|
||||
&scan_args,
|
||||
|
|
@ -457,7 +461,6 @@ fn create_default_scan_args() -> cli::commands::scan::ScanArgs {
|
|||
confidence: ConfidenceLevel::Medium,
|
||||
no_validate: true,
|
||||
access_map: false,
|
||||
access_map_html: None,
|
||||
rule_stats: false,
|
||||
only_valid: false,
|
||||
min_entropy: None,
|
||||
|
|
|
|||
|
|
@ -657,9 +657,8 @@ fn filter_match<'b>(
|
|||
// --- FIX IS HERE ---
|
||||
//
|
||||
// The `validate` function (and thus `{{ MATCH }}`) should *always*
|
||||
// operate on the *full match* (group 0), not just the entropy bytes.
|
||||
// This aligns the scan logic with the unit test's logic.
|
||||
match char_reqs.validate(full_bytes, Some(context), respect_ignore_if_contains) {
|
||||
// operate on the *match* (group 1), not the full match (group 0).
|
||||
match char_reqs.validate(entropy_bytes, Some(context), respect_ignore_if_contains) {
|
||||
//
|
||||
// --- END FIX ---
|
||||
PatternValidationResult::Passed => {}
|
||||
|
|
|
|||
|
|
@ -930,7 +930,6 @@ mod tests {
|
|||
confidence: ConfidenceLevel::Medium,
|
||||
no_validate: false,
|
||||
access_map: false,
|
||||
access_map_html: None,
|
||||
only_valid: false,
|
||||
min_entropy: None,
|
||||
rule_stats: false,
|
||||
|
|
|
|||
|
|
@ -175,7 +175,6 @@ mod tests {
|
|||
confidence: ConfidenceLevel::Medium,
|
||||
no_validate: false,
|
||||
access_map: false,
|
||||
access_map_html: None,
|
||||
rule_stats: false,
|
||||
only_valid: false,
|
||||
min_entropy: None,
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ pub async fn run_async_scan(
|
|||
|
||||
trace!("Args:\n{global_args:#?}\n{args:#?}");
|
||||
let progress_enabled = global_args.use_progress();
|
||||
initialize_environment()?;
|
||||
initialize_environment(progress_enabled)?;
|
||||
|
||||
set_redaction_enabled(args.redact);
|
||||
|
||||
|
|
@ -636,7 +636,7 @@ pub async fn run_async_scan(
|
|||
async fn finalize_access_map(
|
||||
datastore: &Arc<Mutex<FindingsStore>>,
|
||||
collector: AccessMapCollector,
|
||||
args: &scan::ScanArgs,
|
||||
_args: &scan::ScanArgs,
|
||||
) -> Result<()> {
|
||||
let requests = collector.into_requests();
|
||||
|
||||
|
|
@ -654,17 +654,6 @@ async fn finalize_access_map(
|
|||
ds.set_access_map_results(results.clone());
|
||||
}
|
||||
|
||||
if let Some(html_path) = &args.access_map_html {
|
||||
access_map::write_reports(&results, html_path)?;
|
||||
info!("wrote access-map HTML report to {}", html_path.display());
|
||||
}
|
||||
|
||||
// if args.access_map_html.is_none() {
|
||||
// eprintln!(
|
||||
// "Tip: rerun with --access-map-html /path/to/report.html for an interactive access-map viewer."
|
||||
// );
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -728,8 +717,9 @@ fn maybe_hint_access_map(datastore: &Arc<Mutex<FindingsStore>>, args: &scan::Sca
|
|||
}
|
||||
}
|
||||
|
||||
fn initialize_environment() -> Result<()> {
|
||||
let init_progress = ProgressBar::new_spinner();
|
||||
fn initialize_environment(use_progress: bool) -> Result<()> {
|
||||
let init_progress =
|
||||
if use_progress { ProgressBar::new_spinner() } else { ProgressBar::hidden() };
|
||||
init_progress.set_message("Initializing thread pool...");
|
||||
let num_threads = num_cpus::get();
|
||||
// Attempt to initialize the global thread pool only if it hasn't been
|
||||
|
|
@ -841,8 +831,10 @@ pub fn spawn_datastore_writer_thread(
|
|||
pub fn load_and_record_rules(
|
||||
args: &scan::ScanArgs,
|
||||
datastore: &Arc<Mutex<findings_store::FindingsStore>>,
|
||||
use_progress: bool,
|
||||
) -> Result<RulesDatabase> {
|
||||
let init_progress = ProgressBar::new_spinner();
|
||||
let init_progress =
|
||||
if use_progress { ProgressBar::new_spinner() } else { ProgressBar::hidden() };
|
||||
// init_progress.set_message("Compiling rules...");
|
||||
let rules_db = {
|
||||
let loaded = RuleLoader::from_rule_specifiers(&args.rules)
|
||||
|
|
|
|||
|
|
@ -292,6 +292,27 @@ mod gitlab {
|
|||
.code(predicates::function::function(|code: &i32| *code == 0 || *code == 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_gitlab_accepts_global_flags_after_subcommand() {
|
||||
Command::new(assert_cmd::cargo::cargo_bin!("kingfisher"))
|
||||
.args([
|
||||
"scan",
|
||||
"gitlab",
|
||||
"--group",
|
||||
"testgroup",
|
||||
"--include-subgroups",
|
||||
"--list-only",
|
||||
"--quiet",
|
||||
"--access-map",
|
||||
"--no-update-check",
|
||||
"--user-agent-suffix",
|
||||
"cli-test",
|
||||
"--verbose",
|
||||
])
|
||||
.assert()
|
||||
.code(predicates::function::function(|code: &i32| *code != 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_gitlab_with_exclude() {
|
||||
Command::new(assert_cmd::cargo::cargo_bin!("kingfisher"))
|
||||
|
|
|
|||
|
|
@ -136,7 +136,6 @@ fn run_skiplist(skip_regex: Vec<String>, skip_skipword: Vec<String>) -> Result<u
|
|||
confidence: ConfidenceLevel::Low,
|
||||
no_validate: true,
|
||||
access_map: false,
|
||||
access_map_html: None,
|
||||
rule_stats: false,
|
||||
only_valid: false,
|
||||
min_entropy: Some(0.0),
|
||||
|
|
|
|||
|
|
@ -135,7 +135,6 @@ fn test_bitbucket_remote_scan() -> Result<()> {
|
|||
confidence: ConfidenceLevel::Medium,
|
||||
no_validate: false,
|
||||
access_map: false,
|
||||
access_map_html: None,
|
||||
rule_stats: false,
|
||||
only_valid: false,
|
||||
min_entropy: None,
|
||||
|
|
@ -168,7 +167,8 @@ fn test_bitbucket_remote_scan() -> Result<()> {
|
|||
|
||||
let datastore = Arc::new(Mutex::new(FindingsStore::new(clone_dir)));
|
||||
let runtime = Runtime::new()?;
|
||||
let rules_db = Arc::new(load_and_record_rules(&scan_args, &datastore)?);
|
||||
let rules_db =
|
||||
Arc::new(load_and_record_rules(&scan_args, &datastore, global_args.use_progress())?);
|
||||
let update_status = UpdateStatus::default();
|
||||
|
||||
runtime.block_on(async {
|
||||
|
|
|
|||
|
|
@ -155,7 +155,6 @@ rules:
|
|||
confidence: ConfidenceLevel::Low,
|
||||
no_validate: true,
|
||||
access_map: false,
|
||||
access_map_html: None,
|
||||
rule_stats: false,
|
||||
only_valid: false,
|
||||
min_entropy: Some(0.0),
|
||||
|
|
|
|||
|
|
@ -142,7 +142,6 @@ fn test_github_remote_scan() -> Result<()> {
|
|||
confidence: ConfidenceLevel::Medium,
|
||||
no_validate: false,
|
||||
access_map: false,
|
||||
access_map_html: None,
|
||||
rule_stats: false,
|
||||
only_valid: false,
|
||||
min_entropy: None,
|
||||
|
|
@ -177,7 +176,8 @@ fn test_github_remote_scan() -> Result<()> {
|
|||
// Create the runtime first
|
||||
let runtime = Runtime::new().expect("Failed to create Tokio runtime");
|
||||
// Load rules
|
||||
let rules_db = Arc::new(load_and_record_rules(&scan_args, &datastore)?);
|
||||
let rules_db =
|
||||
Arc::new(load_and_record_rules(&scan_args, &datastore, global_args.use_progress())?);
|
||||
let update_status = UpdateStatus::default();
|
||||
// Run the scan using runtime.block_on
|
||||
runtime.block_on(async {
|
||||
|
|
|
|||
|
|
@ -141,7 +141,6 @@ fn test_gitlab_remote_scan() -> Result<()> {
|
|||
confidence: ConfidenceLevel::Medium,
|
||||
no_validate: false,
|
||||
access_map: false,
|
||||
access_map_html: None,
|
||||
rule_stats: false,
|
||||
only_valid: false,
|
||||
min_entropy: None,
|
||||
|
|
@ -174,7 +173,8 @@ fn test_gitlab_remote_scan() -> Result<()> {
|
|||
let datastore = Arc::new(Mutex::new(FindingsStore::new(clone_dir)));
|
||||
let rt = Runtime::new()?;
|
||||
|
||||
let rules_db = Arc::new(load_and_record_rules(&scan_args, &datastore)?);
|
||||
let rules_db =
|
||||
Arc::new(load_and_record_rules(&scan_args, &datastore, global_args.use_progress())?);
|
||||
let update_status = UpdateStatus::default();
|
||||
|
||||
rt.block_on(async {
|
||||
|
|
@ -296,7 +296,6 @@ fn test_gitlab_remote_scan_no_history() -> Result<()> {
|
|||
confidence: ConfidenceLevel::Medium,
|
||||
no_validate: false,
|
||||
access_map: false,
|
||||
access_map_html: None,
|
||||
rule_stats: false,
|
||||
only_valid: false,
|
||||
min_entropy: None,
|
||||
|
|
@ -330,7 +329,8 @@ fn test_gitlab_remote_scan_no_history() -> Result<()> {
|
|||
let datastore = Arc::new(Mutex::new(FindingsStore::new(clone_dir)));
|
||||
let rt = Runtime::new()?;
|
||||
|
||||
let rules_db = Arc::new(load_and_record_rules(&scan_args, &datastore)?);
|
||||
let rules_db =
|
||||
Arc::new(load_and_record_rules(&scan_args, &datastore, global_args.use_progress())?);
|
||||
let update_status = UpdateStatus::default();
|
||||
|
||||
rt.block_on(async {
|
||||
|
|
|
|||
|
|
@ -118,7 +118,6 @@ async fn test_redact_hashes_finding_values() -> Result<()> {
|
|||
confidence: ConfidenceLevel::Low,
|
||||
no_validate: true,
|
||||
access_map: false,
|
||||
access_map_html: None,
|
||||
rule_stats: false,
|
||||
only_valid: false,
|
||||
min_entropy: Some(0.0),
|
||||
|
|
|
|||
|
|
@ -127,7 +127,6 @@ impl TestContext {
|
|||
confidence: ConfidenceLevel::Low,
|
||||
no_validate: true,
|
||||
access_map: false,
|
||||
access_map_html: None,
|
||||
rule_stats: false,
|
||||
only_valid: false,
|
||||
min_entropy: Some(0.0),
|
||||
|
|
@ -270,7 +269,6 @@ async fn test_scan_slack_messages() -> Result<()> {
|
|||
confidence: ConfidenceLevel::Low,
|
||||
no_validate: true,
|
||||
access_map: false,
|
||||
access_map_html: None,
|
||||
rule_stats: false,
|
||||
only_valid: false,
|
||||
min_entropy: Some(0.0),
|
||||
|
|
|
|||
|
|
@ -198,7 +198,6 @@ async fn test_validation_cache_and_depvars() -> Result<()> {
|
|||
confidence: ConfidenceLevel::Low,
|
||||
no_validate: false,
|
||||
access_map: false,
|
||||
access_map_html: None,
|
||||
rule_stats: false,
|
||||
only_valid: false,
|
||||
min_entropy: Some(0.0),
|
||||
|
|
|
|||
|
|
@ -141,7 +141,6 @@ impl TestContext {
|
|||
confidence: ConfidenceLevel::Low,
|
||||
no_validate: true,
|
||||
access_map: false,
|
||||
access_map_html: None,
|
||||
rule_stats: false,
|
||||
only_valid: false,
|
||||
min_entropy: Some(0.0),
|
||||
|
|
@ -273,7 +272,6 @@ impl TestContext {
|
|||
confidence: ConfidenceLevel::Low,
|
||||
no_validate: true,
|
||||
access_map: false,
|
||||
access_map_html: None,
|
||||
rule_stats: false,
|
||||
only_valid: false,
|
||||
min_entropy: Some(0.0),
|
||||
|
|
|
|||
|
|
@ -65,8 +65,7 @@ fn installs_wrapper_without_existing_hook() {
|
|||
|
||||
assert!(wrapper.contains("# Kingfisher pre-commit wrapper"));
|
||||
assert!(wrapper.contains("kingfisher-pre-commit"));
|
||||
assert!(kf_script
|
||||
.contains("kingfisher scan . --staged --quiet --redact --only-valid --no-update-check"));
|
||||
assert!(kf_script.contains("kingfisher scan . --staged --quiet --no-update-check"));
|
||||
assert!(!legacy.exists());
|
||||
}
|
||||
|
||||
|
|
@ -103,8 +102,7 @@ fn preserves_existing_hook_and_runs_it_first() {
|
|||
let log_contents = fs::read_to_string(&log).unwrap();
|
||||
let lines: Vec<_> = log_contents.lines().collect();
|
||||
assert_eq!(lines[0], "legacy");
|
||||
assert!(lines[1]
|
||||
.contains("kingfisher scan . --staged --quiet --redact --only-valid --no-update-check"));
|
||||
assert!(lines[1].contains("kingfisher scan . --staged --quiet --no-update-check"));
|
||||
|
||||
assert!(hooks_path.join("pre-commit.legacy.kingfisher").exists());
|
||||
}
|
||||
|
|
@ -171,17 +169,27 @@ fn errors_outside_git_repository() {
|
|||
|
||||
#[test]
|
||||
fn pre_commit_framework_invokes_kingfisher() {
|
||||
let (_tmp, repo, hooks_path) = init_repo();
|
||||
// Skip this test if `pre-commit` is not available (e.g., in some CI images).
|
||||
if StdCommand::new("pre-commit").arg("--version").output().is_err() {
|
||||
eprintln!(
|
||||
"skipping pre_commit_framework_invokes_kingfisher: `pre-commit` not found in PATH"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let (_tmp, repo, _hooks_path) = init_repo();
|
||||
|
||||
let log = repo.join("hook.log");
|
||||
let bin_dir = repo.join("bin");
|
||||
fs::create_dir_all(&bin_dir).unwrap();
|
||||
|
||||
// Fake kingfisher binary that just logs its argv to hook.log
|
||||
let fake_kingfisher = bin_dir.join("kingfisher");
|
||||
fs::write(&fake_kingfisher, format!("#!/usr/bin/env bash\necho \"$@\" > {}\n", log.display()))
|
||||
.unwrap();
|
||||
StdCommand::new("chmod").args(["+x", fake_kingfisher.to_str().unwrap()]).assert().success();
|
||||
|
||||
// Local pre-commit config that uses `kingfisher` as the entry
|
||||
fs::write(
|
||||
repo.join(".pre-commit-config.yaml"),
|
||||
r#"repos:
|
||||
|
|
@ -198,10 +206,12 @@ fn pre_commit_framework_invokes_kingfisher() {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
// Something for pre-commit to see as a tracked file
|
||||
fs::write(repo.join("README.md"), "demo").unwrap();
|
||||
|
||||
StdCommand::new("uv")
|
||||
.args(["run", "--no-config", "--with", "pre-commit", "pre-commit", "run", "--all-files"])
|
||||
// Run pre-commit directly, with our fake kingfisher at the front of PATH
|
||||
Command::new("pre-commit")
|
||||
.args(["run", "--all-files"])
|
||||
.current_dir(&repo)
|
||||
.env("PATH", format!("{}:{}", bin_dir.display(), std::env::var("PATH").unwrap()))
|
||||
.assert()
|
||||
|
|
@ -244,8 +254,7 @@ fn installer_hook_executes_kingfisher_command() {
|
|||
.success();
|
||||
|
||||
let log_contents = fs::read_to_string(&log).unwrap();
|
||||
assert!(log_contents
|
||||
.contains("kingfisher scan . --staged --quiet --redact --only-valid --no-update-check"));
|
||||
assert!(log_contents.contains("kingfisher scan . --staged --quiet --no-update-check"));
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue